【并发编程九】c++线程同步——互斥(mutex)
- 一、互斥
 - 1、mutex
 - 1.1、mutex
 - 1.2、 lock_guard
 - 1.3、 RAII
 
- 2、std::recursive_mutex
 - 3、std::shared_mutex、std::shared_lock、std::unique_lock
 - 4、std::scoped_lock
 
- 二、条件变量
 - 三、future
 - 四、信号量
 
-  
简介:
上一篇文章,我们讲解了windows、linux、c++标准库的线程和线程同步方法,【并发编程八】线程和线程同步。本篇文章,我们详细的介绍下c++标准库提供的线程同步方法。 -  
参考:
1、https://www.apiref.com/cpp-zh/cpp/thread.html
2、https://en.cppreference.com/w/cpp/thread
3、书籍《c++服务器开发精髓》——张远龙 
一、互斥
- 互斥算法避免多个线程同时访问共享资源。这会避免数据竞争,并提供线程间的同步支持。
前四个定义于头文件 <mutex>,后两个定义于头文件 <shared_mutex> 
| 互斥类型 | 解释 | 
|---|---|
| mutex(C++11) | 提供基本互斥设施(类) | 
| timed_mutex(C++11) | 提供互斥设施,实现有时限锁定(类) | 
| recursive_mutex(C++11) | 提供能被同一线程递归锁定的互斥设施(类) | 
| recursive_timed_mutex(C++11) | 提供能被同一线程递归锁定的互斥设施,并实现有时限锁定(类) | 
| shared_mutex(C++17) | 提供共享互斥设施(类) | 
| shared_timed_mutex(C++14) | 提供共享互斥设施并实现有时限锁定(类) | 
- 通用互斥管理
定义于头文件<mutex> 
| 互斥管理 | 解释 | 
|---|---|
| lock_guard(C++11) | 实现严格基于作用域的互斥体所有权包装器(类模板) | 
| scoped_lock (C++17) | 用于多个互斥体的免死锁 RAII 封装器(类模板) | 
| unique_lock (C++11) | 实现可移动的互斥体所有权包装器(类模板) | 
| shared_lock(C++14) | 实现可移动的共享互斥体所有权封装器(类模板) | 
| defer_lock_t(C++11) try_to_lock_t(C++11) adopt_lock_t(C++11)  | 用于指定锁定策略的标签类型(类) | 
| defer_lock(C++11) try_to_lock(C++11) adopt_lock(C++11)  | 用于指定锁定策略的标签常量(常量) | 
1、mutex
1.1、mutex
- mutex 类是能用于保护共享数据免受从多个线程同时访问的同步原语。
 - 通常不直接使用 std::mutex ,我们通常使用 std::unique_lock 、 std::lock_guard 或 std::scoped_lock (C++17 起)以更加异常安全的方式管理锁定。
 - 此示例展示 mutex 能如何用于在保护共享于二个线程间的 std::map 。
 - 如果不会map加锁,我们输出的map中的值时,可能只有一个值。
 
1.2、 lock_guard
- 类 lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。
 - 创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁lock_guard 并释放互斥。
 - lock_guard 类不可复制。
 
1.3、 RAII
- 资源获取即初始化(Resource Acquisition Is Initialization),或称 RAII,是一种 C++ 编程技术,它将必须在使用前请求的资源(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事物)的生命周期绑定与一个对象的生存期相绑定。
 
#include <iostream>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>
std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;
void save_page(const std::string& url)
{
    // 模拟长页面读取
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::string result = "fake content";
    std::lock_guard<std::mutex> guard(g_pages_mutex);
    g_pages[url] = result;
}
int main()
{
    std::thread t1(save_page, "http://foo");
    std::thread t2(save_page, "http://bar");
    t1.join();
    t2.join();
    // 现在访问g_pages是安全的,因为线程t1/t2生命周期已结束
    for (const auto& pair : g_pages) {
        std::cout << pair.first << " => " << pair.second << '\n';
    }
}
 
2、std::recursive_mutex
- recursive_mutex 类是同步原语,能用于保护共享数据免受从个多线程同时访问。
 - recursive_mutex 提供排他性递归所有权语义:
 - recursive_mutex 的使用场景之一是保护类中的共享状态,而类的成员函数可能相互调用
 - 用法:
recursive_mutex的用处和mutex差不多,用于限制多线程同时访问同一个变量,用来加锁,保证多个线程,同一时刻只能有一个线程在修改变量;和mutex不同的时,recursive_mutex可以允许同一个线程递归的去加锁,线程只有加锁次数和释放次数相同时,才会释放变量的控制权;例如下面的fun2中调用了fun1,但是fun1和fun2中都加了锁,如果使用mutex,在fun1加锁,在fun2中再次加锁,就会造成死锁;所以recursive_mutex可以避免递归嵌套调用时,造成的死锁问题;递归调用不会死锁,同一线程使用recursive_mutex加锁次数和解锁次数相等时释放控制权; 
#include <iostream>
#include <thread>
#include <mutex>
class X {
    std::recursive_mutex m;
    std::string shared;
public:
    void fun1() {
        std::lock_guard<std::recursive_mutex> lk(m);
        shared = "fun1";
        std::cout << "in fun1, shared variable is now " << shared << '\n';
    }
    void fun2() {
        std::lock_guard<std::recursive_mutex> lk(m);
        shared = "fun2";
        std::cout << "in fun2, shared variable is now " << shared << '\n';
        fun1(); // 递归锁在此处变得有用
        std::cout << "back in fun2, shared variable is " << shared << '\n';
    };
};
int main()
{
    X x;
    std::thread t1(&X::fun1, &x);
    std::thread t2(&X::fun2, &x);
    t1.join();
    t2.join();
}
 
3、std::shared_mutex、std::shared_lock、std::unique_lock
C++17 std::shared_mutex的替代方案boost::shared_mutex 
 shared_mutex 类,结合 unique_lock 与 shared_lock 的使用,可以实现读写锁。
通常读写锁需要完成以下功能:
- 1.当 data 被线程A读取时,其他线程仍可以进行读取却不能写入
 - 2.当 data 被线程A写入时,其他线程既不能读取也不能写入
 
对应于功能1,2我们可以这样来描述:
- 1.当线程A获得共享锁时,其他线程仍可以获得共享锁但不能获得独占锁
 - 2.当线程A获得独占锁时,其他线程既不能获得共享锁也不能获得独占锁
 
#include<iostream>
#include <thread>
#include<shared_mutex>
using namespace std;
typedef std::shared_lock<std::shared_mutex> read_lock;
typedef std::unique_lock<std::shared_mutex> write_lock;
std::shared_mutex read_write_mutex;
int32_t g_data =0;
//线程A,读data
void fun1()
{
    for (size_t i = 0; i < 10; i++)
    {
        read_lock rlock(read_write_mutex);
        cout << "t1:g_data=" << g_data << endl;
    }
}
//线程B,读data
void fun2()
{
    for (size_t i = 0; i < 10; i++)
    {
        read_lock rlock(read_write_mutex);
        std::cout <<"t2:g_data="<< g_data << endl;
    }
}
//线程C,写data
void fun3()
{
    for (size_t i = 0; i < 10; i++)
    {
        write_lock rlock(read_write_mutex);
        g_data++;
        std::cout << "t3:g_data=" << g_data << endl;
    }
}
int main()
{
    thread t3(fun3);
    thread t1(fun1);
    thread t2(fun2);
    
 
    t1.join();
    t2.join();
    t3.join();
}
 
输出如下
结果说明:
- 线程1和线程2可以同时读,
(由于io是进程间共享的,可以看到线程1和2同时操作时,在换行endl还没有输出完,两个线程出现了抢占io资源,所以,我们看到了t1和t2输出到了同一行) - 在线程1或者线程2加了锁读时,我们的线程3不可以写的。(所以,我们看不到t3和t1或者t2在同一行)
 - 线程3写入时,不允许线程1和线程3写入。(所以,我们看不到t3和t1或者t2在同一行)
 
简单总结下
- std::shared_lock是读锁,被锁后仍允许其他线程执行同样被shared_lock的代码,
当data被线程A读取时,仍允许其它线程读取data,但是不能写入。 - std::unique_lock是写锁。被锁后不允许其他线程执行被shared_lock或unique_lock的代码。
在写操作时,一般用这个,可以同时限制unique_lock的写和share_lock的读。 
4、std::scoped_lock
lock_guard:更加灵活的锁管理类模板,构造时是否加锁是可选的,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁。
scope_lock:严格基于作用域(scope-based)的锁管理类模板,构造时是否加锁是可选的(不加锁时假定当前线程已经获得锁的所有权),析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁。
share_lock:用于管理可转移和共享所有权的互斥对象。
- scope_lock和lock_guard相比,可以简单的理解,在生命周期内,scope_lock不允许手动加锁和释放锁,而lock_guard可以。
 
二、条件变量
参见《【并发编程九】c++线程同步——条件变量(condition_variable)》(书写中。。。还未完成)
三、future
(书写中。。。还未完成)
四、信号量
(书写中。。。还未完成)






















