C++多线程编程(互斥锁、条件变量)

互斥锁std::mutex

  C++中常见的cout是一个共享资源,如果在多个线程同时执行cout,会发现很奇怪的问题,解决办法就是要对cout这个共享资源进行保护。

  在C++中,可以使用互斥锁std::mutex进行资源保护,头文件是#include <mutex>,共有两种操作:锁定(lock)与解锁(unlock)。

  将cout重新封装成一个线程安全的函数

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

std::mutex mu;
// 使用锁保护
void shared_print(string msg, int id) {
    mu.lock(); // 上锁
    cout << msg << id << endl;
    mu.unlock(); // 解锁
}

void function_1() {
    for(int i=0; i>-100; i--)
        shared_print(string("From t1: "), i);
}

int main()
{
    std::thread t1(function_1);

    for(int i=0; i<100; i++)
        shared_print(string("From main: "), i);

    t1.join();
    return 0;
}

  

互斥锁std::lock_guard

  但是,如果mu.lock()mu.unlock()之间的语句发生了异常,unlock()语句没有机会执行!导致导致mu一直处于锁着的状态,其他使用shared_print()函数的线程就会阻塞。解决这个问题也很简单,使用c++中常见的RAII技术,即获取资源即初始化(Resource Acquisition Is Initialization)技术,这是C++中管理资源的常用方式。简单的说就是在类的构造函数中创建资源,在析构函数中释放资源,因为就算发生了异常,C++也能保证类的析构函数能够执行。C++库提供了std::lock_guard类模板,使用方法如下:

void shared_print(string msg, int id) {
    //构造时上锁,析构时释放锁
    std::lock_guard<std::mutex> guard(mu);
    cout << msg << id << endl;
}
  如果你将某个mutex上锁了,却一直不释放,另一个线程访问该锁保护的资源的时候,就会发生死锁,这种情况下使用lock_guard可以保证析构的时候能够释放锁,然而,当一个操作需要使用两个互斥元的时候,仅仅使用lock_guard并不能保证不会发生死锁
1. 可以比较mutex的地址,每次都先锁地址小的
if(&_mu < &_mu2){
    _mu.lock();
    _mu2.lock();
}
else {
    _mu2.lock();
    _mu.lock();
}

2. 使用层次锁,将互斥锁包装一下,给锁定义一个层次的属性,每次按层次由高到低的顺序上锁

  C++标准库中提供了std::lock()函数,能够保证将多个互斥锁同时上锁

std::lock(_mu, _mu2);

  这两种办法其实都是严格规定上锁顺序,只不过实现方式不同。同时,lock_guard也需要做修改,因为互斥锁已经被上锁了,那么lock_guard构造的时候不应该上锁,只是需要在析构的时候释放锁就行了,使用std::adopt_lock表示无需上锁

std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);

std::unique_lock

  互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域。lock_guard只能保证在析构的时候执行解锁操作,l没有提供加锁和解锁的接口。

  unique_lock提供了lock()unlock()接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁,可以使用std::defer_lock设置初始化的时候不进行默认的上锁操作

void shared_print(string msg, int id) {
    std::unique_lock<std::mutex> guard(_mu, std::defer_lock);
    //do something 1

    guard.lock();
    // do something protected
    guard.unlock(); //临时解锁

    //do something 2

    guard.lock(); //继续上锁
    // do something 3
    f << msg << id << endl;
    cout << msg << id << endl;
    // 结束时析构guard会临时解锁
}

条件变量std::condition_variable

  C++11中提供了#include <condition_variable>头文件,其中的std::condition_variable可以和std::mutex结合一起使用

  wait(),可以让线程陷入休眠状态,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作;

  notify_one()唤醒处于wait中的其中一个条件变量(可能当时有很多条件变量都处于wait状态);

  notify_all(),可以同时唤醒所有处于wait状态的条件变量#include <iostream#include <dequ#include <thread>

#include <mutex>
#include <condition_variable>

std::deque<int> q;
std::mutex mu;
std::condition_variable cond;

//生产者
void function_1() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);//上锁
        q.push_front(count);//往队头放数据
        locker.unlock();//解锁
        cond.notify_one();  // 通知一个等待的线程
        std::this_thread::sleep_for(std::chrono::seconds(1));
        count--;
    }
}

//消费者
void function_2() {
    int data = 0;
    while ( data != 1) {
        std::unique_lock<std::mutex> locker(mu);//上锁
        while(q.empty())
            cond.wait(locker); //解锁并等待通知
        data = q.back();//取队尾数据
        q.pop_back();//删除队尾数据
        locker.unlock();//解锁
        std::cout << data << std::endl;
    }
}

int main() {
    std::thread t1(function_1);//生产者
    std::thread t2(function_2);//消费者
    t1.join();
    t2.join();
    return 0;
}
  在function_2中,在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞。
 

原文链接:https://www.jianshu.com/p/c1dfa1d40f53
原文地址:https://www.cnblogs.com/yongjin-hou/p/14529320.html