C++ 线程

C++ 线程

成员函数

c++ 复制代码
1)构造函数:std::mutex 不允许拷贝构造,也不允许 move 拷贝,最初产生的mutex 对象是处于 unlocked 状态的。
2)lock():调用线程将锁住该互斥量,线程调用该函数会发生以下 3 种情况:
    (a)如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock 之前,该线程一直拥有该锁。
    (b)如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
    (c)如果当前互斥量被当前调用线程锁住,则会产生死锁。
3)unlock():解锁,释放对互斥量的所有权。
4)try_lock():尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会
    被阻塞,线程调用该函数会出现下面 3 种情况:
    (a)如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
    (b)如果当前互斥量被其他线程锁住,false,而并不会被阻塞掉。
    (c)如果当前互斥量被当前调用线程锁住,则会产生死锁。

线程创建

C++线程的启动,只需要#include 即可。线程对象的创建,意味着线

程的开始。

c++ 复制代码
//todo 线程
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

// 定义一个函数,该函数将在新线程中运行
void func()
{
    // 输出当前线程的ID
    cout << "thread id:" << this_thread::get_id() << endl;
    cout << "线程运行" << endl;
    // 让当前线程休眠10秒
    this_thread::sleep_for(chrono::seconds(10));
}

int main()
{
    // 输出主线程的ID
    cout << "main thread id:" << this_thread::get_id() << endl;
    // 创建一个新线程,运行func函数
    thread t(func);
    // 等待新线程完成(即等待func函数执行完毕)
    t.join();
    cout << "线程运行结束" << endl;

    return 0;
}
c++ 复制代码
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

// 定义一个函数,该函数将在新线程中运行
void func(int & n,string & c)
{
    for (int i = 0; i < n; ++i) {
        cout<<c<<endl;
    }
    // 修改传入的参数n和c的值
    n = 10;
    c = "aa";

}

int main()
{
    int n=5;string str="cccc";
    // 创建一个新线程t,并传入函数func和参数n、str的引用
    //std::ref 是 C++ 标准库中的一个函数模板,位于 <functional> 头文件中。
    // 它的作用是创建一个 std::reference_wrapper 类型的对象,
    // 该对象可以用来传递引用,而不是传递值。
    thread t(func, std::ref(n), std::ref(str));
    t.join();
    cout<< n<<" "<<str<<endl;//10 aa
    return 0;
}

线程分离

c++ 复制代码
#if 1
//todo 线程
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

// 定义一个函数,该函数将在新线程中运行
void func(int  n,string c)
{
    for (int i = 0; i < n; ++i) {
        cout<<c<<endl;
    }

}

int main()
{
    int n=5;string str="cccc";
    // 创建一个新线程t,并传入函数func和参数n、str
    thread t(func, n, str);

    // 将线程t设置为分离状态,使其在后台运行
    t.detach();

    // 检查线程t是否仍然可连接(即是否仍在运行)
    if (t.joinable()) {
        cout << "等待线程执行完毕" << endl;
        // 如果线程仍可连接,则等待其执行完毕
        t.join();
    }
    return 0;
}

# endif

批量创建

c++ 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

using namespace std;

// 定义一个宏,表示要创建的线程数量
#define N 1000

// 定义一个函数,该函数将在新线程中运行
void foo(int id)
{
    cout << "Thread " << id << " started. ID: " << this_thread::get_id() << endl;
    // 让当前线程休眠一段时间,模拟工作负载
    this_thread::sleep_for(chrono::seconds(1));
    cout << "Thread " << id << " finished." << endl;
}

int main()
{
    // 创建一个存储线程对象的向量
    vector<thread> threads;
    cout<<"thread 大小:"<< sizeof(thread)<<endl;//8
    // 预分配向量空间,避免多次内存分配
    // reserve 函数用于预分配内存空间,使得向量的容量(capacity)至少为指定的值。这样,在向向量中添加元素时,如果元素数量不超过预分配的容量,就不会触发内存重新分配,从而提高性能。
    threads.reserve(N);

    // 循环创建并启动N个线程
    for (int i = 0; i < N; ++i)
    {
        // 创建一个新线程,并执行foo函数,传递线程编号作为参数
        //emplace_back 函数用于在向量的末尾直接构造一个元素。它通过可变参数模板(variadic templates)接受构造函数的参数,并在向量的末尾直接调用元素的构造函数,从而避免复制或移动操作。
        threads.emplace_back(foo, i);
    }

    // 等待所有线程执行完毕
    for (auto& th : threads)
    {
        if (th.joinable())
        {
            th.join();
        }
    }

    cout << "All threads finished." << endl;

    return 0;
}

c++ 复制代码
//todo 线程同步

#include <iostream>
#include <mutex>
#include <thread>
using namespace std;


volatile int counter(0); //volatile 关键字在 C++ 中用于告诉编译器,某个变量的值可能会在程序的控制之外被改变,因此编译器不应该对该变量进行优化,到寄存器。

mutex mtx;//锁

void increase10Ktime()
{
    for(int i=0; i<10000; i++)
    {
        mtx.lock();//加锁
        counter++;
        mtx.unlock();//解锁
    }
}
int main()
{
    thread ths[10];
    for(int i=0; i<10; i++)
    {
        ths[i] = thread(increase10Ktime);
    }
    for(auto &th:ths)
        th.join();

    cout<<"after successful increase :"<<counter<<endl;
    return 0;
}

try_lock/unclok

c++ 复制代码
# if 1
//todo 线程同步 try_lock/unclok

#include <iostream>
#include <mutex>
#include <thread>
using namespace std;


volatile int counter(0); //volatile 关键字在 C++ 中用于告诉编译器,某个变量的值可能会在程序的控制之外被改变,因此编译器不应该对该变量进行优化,到寄存器。

mutex mtx;//锁

void increase10Ktime()
{
    for(int i=0; i<10000; i++)
    {
        mtx.try_lock();
        counter++;
        mtx.unlock();//解锁
    }
}
int main()
{
    thread ths[10];
    for(int i=0; i<10; i++)
    {
        ths[i] = thread(increase10Ktime);
    }
    for(auto &th:ths)
        th.join();

    cout<<"after successful increase :"<<counter<<endl;
    return 0;
}


# endif

lock_guard

c++ 复制代码
在 lock_guard 对象构造时,传入的 Mutex 对象(即它所管理的 Mutex 对象)会被当前线程锁住。
在 lock_guard 对象被析构时,它所管理的 Mutex 对象会自动解锁,由于不需要程序员手动调用 lock 和 unlock 对 Mutex 进行上锁和解锁操作,
因此这也是最简单安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的
Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常
处理代码。

与 Mutex RAII 相关,方便线程对互斥量上锁。using a local lock_guard to lock
mtx guarantees unlocking on destruction / exception:

引例:

c++ 复制代码
//todo RAII
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 声明一个互斥锁对象
mutex mtx;

// 函数:检查一个整数是否为偶数,并输出结果
void printEven(int i)
{
    if( i % 2 == 0)  // 如果i是偶数
        cout << i << " is even" << endl;  // 输出i是偶数
    else
        throw logic_error("not even");  // 如果i不是偶数,抛出一个逻辑错误异常
}

// 函数:打印线程ID,并处理可能的异常
void printThreadId(int id)
{
    try{
        mtx.lock();  // 锁定互斥锁,确保线程安全
        printEven(id);  // 调用printEven函数,检查并输出id是否为偶数
        mtx.unlock();  // 解锁互斥锁
    }catch(logic_error & ){  // 捕获逻辑错误异常
        cout << "exception caught" << endl;  // 输出异常信息
    }
}

int main()
{
    thread ths[10];  // 声明一个包含10个线程对象的数组

    // 创建10个线程
    for(int i = 0; i < 10; i++)
    {
        ths[i] = thread(printThreadId, i + 1);  // 启动线程,调用printThreadId函数,参数为i+1
    }

    // 等待所有线程完成
    for(auto & th : ths)
        th.join();  // 主线程等待每个子线程完成

    return 0;
}
 
获取锁后,抛出异常,就不会解锁

lock_guard

c++ 复制代码
lock_guard 的使用体现了 RAII(Resource Acquisition Is Initialization)思想。RAII 是 C++ 中的一种编程范式,核心思想是通过对象的生命周期来管理资源,确保资源在对象构造时获取,在对象析构时释放。

RAII 的核心概念
    资源获取即初始化:在对象构造时获取资源,在对象析构时释放资源。
    自动管理资源:通过对象的生命周期自动管理资源,避免资源泄漏和手动管理资源的复杂性。
    
lock_guard 如何体现 RAII
    构造时获取资源:lock_guard 在其构造函数中锁定互斥锁。
    析构时释放资源:lock_guard 在其析构函数中解锁互斥锁。
    
这意味着当 lock_guard 对象被创建时,互斥锁会被自动锁定;当 lock_guard 对象离开其作用域(例如函数返回或异常抛出)时,互斥锁会被自动解锁。
c++ 复制代码
# if 1
//todo RAII
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 声明一个互斥锁对象
mutex mtx;

// 函数:检查一个整数是否为偶数,并输出结果
void printEven(int i)
{
    if( i % 2 == 0)  // 如果i是偶数
        cout << i << " is even" << endl;  // 输出i是偶数
    else
        throw logic_error("not even");  // 如果i不是偶数,抛出一个逻辑错误异常
}

// 函数:打印线程ID,并处理可能的异常
void printThreadId(int id)
{
    try{
        lock_guard<mutex> lck(mtx);//它在其构造函数中锁定互斥锁,并在其析构函数中解锁互斥锁。这样可以确保在离开作用域时自动释放锁,避免忘记解锁导致的死锁问题。
        printEven(id);  // 调用printEven函数,检查并输出id是否为偶数

    }catch(logic_error & ){  // 捕获逻辑错误异常
        cout << "exception caught" << endl;  // 输出异常信息
    }
}

int main()
{
    thread ths[10];  // 声明一个包含10个线程对象的数组

    // 创建10个线程
    for(int i = 0; i < 10; i++)
    {
        ths[i] = thread(printThreadId, i + 1);  // 启动线程,调用printThreadId函数,参数为i+1
    }

    // 等待所有线程完成
    for(auto & th : ths)
        th.join();  // 主线程等待每个子线程完成

    return 0;
}


# endif
c++ 复制代码
输出
exception caught
2 is even
exception caught
10 is even
exception caught
6 is even
exception caught
8 is even
exception caught
4 is even

死锁

死锁的原因是,container 试图多次去获取锁己获得的锁。std::recursive_mutex 允

许多次获取相同的 mutex。

死锁:

c++ 复制代码
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
using namespace std;

// 模板类:容器,用于存储元素并提供线程安全的添加和访问方法
template <typename T>
class container
{
public:
    // 添加单个元素到容器中
    void add(T element)
    {
        _mtx.lock();  // 锁定互斥锁,确保线程安全
        _elements.push_back(element);  // 将元素添加到向量中
        _mtx.unlock();  // 解锁互斥锁
    }

    // 添加多个元素到容器中
    void addrange(int num, ...)
    {
        va_list arguments;  // 声明一个可变参数列表
        va_start(arguments, num);  // 初始化可变参数列表
        for (int i = 0; i < num; i++)
        {
            _mtx.lock();  // 锁定互斥锁,确保线程安全
            add(va_arg(arguments, T));  // 从可变参数列表中获取一个元素并添加到容器中
            _mtx.unlock();  // 解锁互斥锁
        }
        va_end(arguments);  // 结束可变参数列表
    }

    // 输出容器中的所有元素
    void dump()
    {
        _mtx.lock();  // 锁定互斥锁,确保线程安全
        for(auto e : _elements)
            cout << e << endl;  // 输出每个元素
        _mtx.unlock();  // 解锁互斥锁
    }

private:
    mutex _mtx;  // 互斥锁,用于保护共享数据
    vector<T> _elements;  // 存储元素的向量
};

// 线程函数:向容器中添加随机数
void func(container<int>& cont)
{
    cont.addrange(3, rand(), rand(), rand());  // 向容器中添加3个随机数
}

int main()
{
    srand((unsigned int)time(0));  // 初始化随机数生成器
    container<int> cont;  // 创建一个存储整数的容器

    // 创建3个线程,每个线程调用func函数向容器中添加随机数
    thread t1(func, ref(cont));
    thread t2(func, ref(cont));
    thread t3(func, ref(cont));

    // 等待所有线程完成
    t1.join();
    t2.join();
    t3.join();

    cont.dump();  // 输出容器中的所有元素
    return 0;
}

递归锁 (unique_lock /recursive_mutex)

recursive_mutex _mtx;

recursive_mutex 是 C++ 标准库中的一种互斥锁,允许同一个线程多次锁定同一个互斥锁,而不会导致死锁。这与普通的 mutex 不同,普通的 mutex 在同一个线程中多次锁定会导致死锁。
unique_lock<recursive_mutex> lck(_mtx);

unique_lock 是 C++11 引入的一种智能锁,用于简化互斥锁的管理。与 lock_guard 类似,unique_lock 也提供了 RAII 风格的锁管理,但它更加灵活,支持延迟锁定、锁的所有权转移和手动解锁等操作。

改进:

c++ 复制代码
include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
using namespace std;

// 模板类:容器,用于存储元素并提供线程安全的添加和访问方法
template <typename T>
class container
{
public:
    // 添加单个元素到容器中
    void add(T element)
    {
        unique_lock<recursive_mutex> lck(_mtx);
        _elements.push_back(element);  // 将元素添加到向量中

    }
    // 添加多个元素到容器中
    void addrange(int num, ...)
    {
        va_list arguments;  // 声明一个可变参数列表
        va_start(arguments, num);  // 初始化可变参数列表
        for (int i = 0; i < num; i++)
        {
            unique_lock<recursive_mutex> lck(_mtx);
            add(va_arg(arguments, T));  // 从可变参数列表中获取一个元素并添加到容器中

        }
        va_end(arguments);  // 结束可变参数列表
    }
    // 输出容器中的所有元素
    void dump()
    {
        unique_lock<recursive_mutex> lck(_mtx);//unique_lock 是 C++11 引入的一种智能锁,用于简化互斥锁的管理。与 lock_guard 类似,unique_lock 也提供了 RAII 风格的锁管理,但它更加灵活,支持延迟锁定、锁的所有权转移和手动解锁等操作。
        for(auto e : _elements)
            cout << e << endl;  // 输出每个元素

    }

private:
    recursive_mutex _mtx;  // recursive_mutex 是 C++ 标准库中的一种互斥锁,允许同一个线程多次锁定同一个互斥锁,而不会导致死锁。这与普通的 mutex 不同,普通的 mutex 在同一个线程中多次锁定会导致死锁。
    vector<T> _elements;  // 存储元素的向量
};

// 线程函数:向容器中添加随机数
void func(container<int>& cont)
{
    cont.addrange(3, rand(), rand(), rand());  // 向容器中添加3个随机数
}

int main()
{
    srand((unsigned int)time(0));  // 初始化随机数生成器
    container<int> cont;  // 创建一个存储整数的容器

    // 创建3个线程,每个线程调用func函数向容器中添加随机数
    thread t1(func, ref(cont));
    thread t2(func, ref(cont));
    thread t3(func, ref(cont));

    // 等待所有线程完成
    t1.join();
    t2.join();
    t3.join();

    cont.dump();  // 输出容器中的所有元素
    return 0;
}
c++ 复制代码
输出
6334
18467
41
6334
18467
41
6334
18467
41

条件变量(condition variable)

条件变量(condition variable)是利用线程间共享的全局变量进行同步的一种机

制,

主要包括两个动作:

一个线程等待某个条件为真,而将自己挂起;

另一个线程使的条件成立,并通知等待的线程继续。

为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

C++11 中引入了条件变量,其相关内容均在<condition_variable>中。

这里主要介绍 std::condition_variable 类。

条件变量 std::condition_variable 用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。

std::condition_variable 需 要 与 std::unique_lock 配 合 使 用 。

std::condition_variable 效果上相当于包装了 pthread 库中的 pthread_cond_*()系列的函数。

当 std::condition_variable 对象的某个wait函数被调用的时候 , 它使用std::unique_lock(通过 std::mutex)来锁住当前线程。

当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒

当前线程。

c++ 复制代码
# if 1
//todo 条件变量(condition variable)
/*
10 个线程,争相去打印,传入的 id,第一个线程但是获得锁的,但为条件不满足,
采用条件变量出让锁,等待条件,其它线程亦是如此。延时后的主要线程,获得锁,将
条件置为真,并能过条件变量唤醒所有等待在条件变量上的 10 个线程。此时 10 个线
程再次争相去获取锁,然后,判断条件为真,然后打印,释放锁给其它线程。
*/
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

// 全局变量,用于指示线程是否可以开始工作
bool ready = false;

// 互斥锁,用于保护共享变量
mutex mtx;

// 条件变量,用于线程间的同步
condition_variable cv;

// 线程函数:打印线程ID
void printId(int id) {
    unique_lock<mutex> lck(mtx);  // 创建unique_lock并锁定互斥锁
    while (!ready) {  // 检查ready变量是否为true
        cv.wait(lck);  // 如果ready为false,则线程进入等待状态,并解锁互斥锁
    }
    cout << "id= " << id << endl;  // 打印线程ID
}  // unique_lock在离开作用域时自动解锁

// 函数:通知所有线程开始工作
void go() {
    unique_lock<mutex> lck(mtx);  // 创建unique_lock并锁定互斥锁
    ready = true;  // 设置ready变量为true,表示线程可以开始工作
    cv.notify_all();  // 通知所有等待的线程
}  // unique_lock在离开作用域时自动解锁

int main() {
    thread ths[10];  // 声明一个包含10个线程对象的数组

    // 创建10个线程,每个线程调用printId函数,参数为线程ID
    for (int i = 0; i < 10; ++i) {
        ths[i] = thread(printId, i);
    }

    // 主线程休眠5秒
    this_thread::sleep_for(chrono::seconds(5));

    // 通知所有线程开始工作
    go();

    // 等待所有线程完成
    for (auto &t : ths) {
        t.join();
    }

    return 0;
}


# endif
相关推荐
百事老饼干2 分钟前
Java[面试题]-真实面试
java·开发语言·面试
可均可可24 分钟前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
杨荧40 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
白子寰1 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_011 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj1 小时前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。1 小时前
c++多线程
java·开发语言
-Even-1 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
小政爱学习!1 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript