C++并发编程实战笔记(二)互斥量与同步机制(条件变量,期值)

 
Category: C_C++

写在前面

C++并发编程实战的三四章内容的总结.

数据竞争(条件竞争)

看下面的多线程读写示例

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

int i{};

void f() {
    int num = 10000;
    for (int n{}; n < num; ++n) i = i + 1;
}

int main(int argc, char *argv[]) {
    thread t1(f);
    thread t2(f);
    t1.join();
    t2.join();
    cout << i << endl; // 10719

    return 0;
}

结果并不是 20000, 因为多线程会竞争读写访问的数据, 导致实际写入次数少于循环数量.

下面的互斥量, 条件变量等都可以用来解决线程数据的同步问题.

互斥

互斥量

mutex

lock_guard

互斥锁引发的死锁问题

发生死锁的四个条件

同时满足下面的四个条件才会发生死锁

  • 互斥条件: 多个线程不能同时使用一个资源
  • 持有并等待条件: 线程已经持有的线程被别的线程访问, 别的线程只能阻塞
  • 不可剥夺条件: 自己使用完资源之前, 资源不会被别的线程获取
  • 环路等待条件: 两个(或多个)线程获取资源的顺序存在环路

不用互斥锁也会死锁吗: 会的. 看下面的例子:

  void bar();
  
  void foo() {
      thread t1(bar);
      t1.join();
      cout << "foo done" << endl;
  }
  void bar() {
      thread t2(foo);
      t2.join();
      cout << "bar done" << endl;
  }
  
  int main() {
      bar();
      foo();
      return 0;
  }

互相等待对方结束的两个线程.

事实上这个程序会抛出异常system_error, 不会一直等待.

what():  Resource temporarily unavailable

下面是使用 mutex 导致死锁的实例

单线程死锁

#include <mutex>
#include <iostream>
#include <thread>
using namespace std;
void t1();
void t2();
mutex m1, m2;

// 单线程死锁
void t1() {
    lock_guard<std::mutex> l1(m1);
    t2();
}
void t2() {
    lock_guard<std::mutex> l2(m2);
    t1();
}

int main(int argc, char *argv[]) {
    t1();
    t2();
    return 0;
}

多线程死锁

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1, mutex2;

void ThreadA() {
    mutex1.lock();
    std::cout << "Thread A has mutex1" << std::endl;

    // 休眠 1s,模拟线程 A 执行其他操作
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 尝试获取 mutex2 锁
    std::cout << "Thread A is waiting for mutex2" << std::endl;
    mutex2.lock();
    std::cout << "Thread A has mutex2" << std::endl;

    mutex2.unlock();
    mutex1.unlock();
}

void ThreadB() {
    mutex2.lock();
    std::cout << "Thread B has mutex2" << std::endl;

    // 休眠 1s,模拟线程 B 执行其他操作
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 尝试获取 mutex1 锁
    std::cout << "Thread B is waiting for mutex1" << std::endl;
    mutex1.lock();
    std::cout << "Thread B has mutex1" << std::endl;

    mutex1.unlock();
    mutex2.unlock();
}

int main() {
    std::thread threadA(ThreadA);
    std::thread threadB(ThreadB);

    threadA.join();
    threadB.join();

    return 0;
}

unique_lock

灵活加锁, 但是也耗费一定的资源. (相较于 lock_guard 而言)

不一定始终占有与之关联的互斥量.

具有更细粒度的控制级别


#include <thread>
#include <mutex>


class some_big_object {};

void swap(some_big_object& lhs, some_big_object& rhs);

class X {
private:
    some_big_object some_detail;
    std::mutex m;

public:
    X(some_big_object const& sd) : some_detail(sd) {}

    // 执行内部数据互换(避免死锁)
    // c++17 scoped_lock version
    friend void swap(X& lhs, X& rhs) {
        if (&rhs == &lhs) return;
        std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
        std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
        std::lock(lock_a, lock_b);
        swap(lhs.some_detail, rhs.some_detail);
    }
};
  1. defer_lock: 将互斥保留为未加锁状态.
  2. std::lock(): 此时才开始上锁.

双检查锁初始化资源的问题

shared_lock

共享锁(读锁)

多个线程可以同时锁住同一个std::shared_mutex.

递归加锁

同步

条件变量(condition_variable)

利用条件变量等待事件完成

期值(future)

获取异步任务的返回值

#include <future>
#include <iostream>
using namespace std;

int find_ans() { return 42; }


int main(int argc, char *argv[]) {
    // async 和 thread 的创建方式类似, 都是先传入函数签名(可调用对象), 然后传入函数参数
    future<int> ans = std::async(find_ans);
    cout << "ans is " << ans.get() << endl; // ans is 42
    return 0;
}

异步 (async)