多线程 - 线程锁
多线程 - 线程锁
互斥量(mutex)
简概
简概
- 互斥量是个类对象,理解成一把锁
 - 使用互斥量要小心,多了影响效率、少了则不安全
 
使用
std::mutex mymutex;,创建类成员变量lock(),成员函数能够对线程进行加锁。同一时间只有一个线程能够锁成功;
锁成功时会返回true,若没锁成功则阻塞会一直尝试调用lock()unlock(),成员函数能够对线程进行解锁- 使用步骤:lock -> 操作共享数据 -> unlock,其中lock和unlock要成对使用
 
智能互斥量
std::lock_guard类模板,能自动unlock(防止你忘记unlock、有点像智能指针自动释放内存)- 原理:
std::lock_guard<std::mutex> sbguard(my_mutex);生成的互斥量对象,在构造函数和析构函数调用时,自动加锁和解锁
是对局部变量的巧妙运用 - 缺点:不太灵活,无法自由控制解锁的位置,不能手动unlock。但可以配合
{}来提高灵活度 
错误例程(边读边写,会崩溃)
#include <iostream>
#include <thread>			// 线程头文件
using namespace std;
class A{
public:   
    // 收集数据的线程
    void inMsgRecQueue()
    {
        for(int i=0; i<10000; ++i)			// 增加崩溃几率 
        { 
            cout << "_"; 
            msgRecvQueue.push_back(i);		// 写操作是非原子性的
        } 
    }  
    // 取出数据的线程
    void outMsgRecQueue() 
    {      
        for(int i=0; i<10000; ++i)			// 增加崩溃几率   
        {       
            if(!msgRecvQueue.empty())      
            {          
                msgRecvQueue.front();		// 返回第一个元素但不检查是否存在   
                msgRecvQueue.pop_front();	// 移除第一个元素但不返回,写操作是非原子性的
                cout << "O"; 
            }  
            else    
            {            
                cout << "空";    
            }      
        }  
    }
private  
    std::list<int> msgRecvQueue;			// 收到的消息列表
} 
int main(){
    A a;
    thread threadI(&A::inMsgRecQueue, &a);	// 成员函数作为线程初始函数
    thread threadO(&A::outMsgRecQueue, &a);	// 成员函数作为线程初始函数
    threadI.json();
    threadO.json();
    cout << "《主线程》" << endl 
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
    return 0;
}加锁例程(lock/unlock)
#include <iostream>
#include <thread>			// 线程头文件
using namespace std;
class A
{
public:
    // 收集数据的线程
    void inMsgRecQueue()
    {  
        for(int i=0; i<10000; ++i)			// 增加崩溃几率 
        {      
            cout << "_";
            mymutex.lock();					// 【互斥量】加锁  
            msgRecvQueue.push_back(i);		// 写操作是非原子性的 
            mymutex.unlock();				// 【互斥量】解锁 
        } 
    }   
    // 取出数据的线程   
    void outMsgRecQueue()  
    {    
        for(int i=0; i<10000; ++i)			// 增加崩溃几率  
        {          
            mymutex.lock();					// 【互斥量】加锁   
            if(!msgRecvQueue.empty())		// 读        
            {            
                mymutex.lock();				// 【互斥量】加锁        
                msgRecvQueue.front();		// 返回第一个元素但不检查是否存在    
                msgRecvQueue.pop_front();	// 移除第一个元素但不返回,写操作是非原子性的   
                cout << "O";      
            }      
            mymutex.unlock();				// 【互斥量】解锁       
            else       
            {           
                cout << "空";    
            }      
        }  
    }
private 
    std::list<int> msgRecvQueue;			// 收到的消息列表  
    std::mutex mymutex;						// 【互斥量】创建一个互斥量成员变量
}  
int main()
{   
    A a; 
    thread threadI(&A::inMsgRecQueue, &a);	// 成员函数作为线程初始函数
    thread threadO(&A::outMsgRecQueue, &a);	// 成员函数作为线程初始函数
    threadI.json(); 
    threadO.json();   
    cout << "《主线程》" << endl  
        <<"《线程id》"<<std::this_thread:get_id()<<endl; 
    return 0;
}智能互斥量
lock_guard
lock_guard
加锁例程
原理:std::lock_guard<std::mutex> sbguard(my_mutex);生成的互斥量对象,在构造函数和析构函数调用时,自动加锁和解锁
class A
{
public:   
    // 收集数据的线程  
    void inMsgRecQueue() 
    {     
        for(int i=0; i<10000; ++i)			// 增加崩溃几率       
        {           
            cout << "_";      
            std::lock_guard<std::mutex> sbguard(my_mutex);		// 【智能互斥量】     
            msgRecvQueue.push_back(i);		// 写操作是非原子性的    
        }    
    }   
    // 取出数据的线程    
    void outMsgRecQueue()  
    {      
        for(int i=0; i<10000; ++i)			// 增加崩溃几率    
        {      
            if(!msgRecvQueue.empty())		// 读     
            {             
                std::lock_guard<std::mutex> sbguard(my_mutex);	// 【智能互斥量】    
                msgRecvQueue.front();		// 返回第一个元素但不检查是否存在    
                msgRecvQueue.pop_front();	// 移除第一个元素但不返回,写操作是非原子性的  
                cout << "O";        
            }      
            else         
            {        
                cout << "空";   
            }    
        } 
    }
private  
    std::list<int> msgRecvQueue;			// 收到的消息列表  
    std::mutex mymutex;						// 【互斥量】创建一个互斥量成员变量
}lock_guard 第二个参数
第二个参数:该参数是一个结构体对象,起一个标记的作用
std::adopt_lock作用:表示这个互斥量已经lock()了,不需要再lock一遍,只要负责析构时解锁就行了
应用:创建智能互斥量对象(组合)
std::lock(mutexA, mutexB); // 先组合锁 std::lock_guard<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::lock_guard<std::mutex> sbguard(mutexB, std::adopt_lock);
unique_lock
unique_lock
- 简概 
unique_lock是个类模板。和lock_guard的作用类似,但更灵活
 - 比较unique_lock、lock_guard 
- 选用:一般选用lock_guard
 - 优点:更灵活。可以在代码中自由加锁解锁(锁锁住代码的多少,称为锁的
粒度的粗细,粒度越细执行效率越高) - 缺点:效率低一点,内存占用多点,但差别不大
 
 - 使用 
- 可直接替换lock_guard使用
 - 此时,功能和lock_guard没什么区别
 
 
#include <iostream>
#include <thread>			// 线程头文件
using namespace std;
class A
{
public:   
    // 收集数据的线程 
    void inMsgRecQueue()   
    {      
        for(int i=0; i<10000; ++i)			// 增加崩溃几率   
        {        
            cout << "_";   
            std::unique_lock<std::mutex> sbguard(my_mutex);	// 【unique_lock】  
            msgRecvQueue.push_back(i);		// 写操作是非原子性的    
        }  
    }  
    // 取出数据的线程   
    void outMsgRecQueue() 
    {     
        for(int i=0; i<10000; ++i)			// 增加崩溃几率   
        {          
            std::unique_lock<std::mutex> sbguard(my_mutex);	// 【unique_lock】            
            if(!msgRecvQueue.empty())		// 读       
            {            
                msgRecvQueue.front();		// 返回第一个元素但不检查是否存在  
                msgRecvQueue.pop_front();	// 移除第一个元素但不返回,写操作是非原子性的       
                cout << "O";     
            }           
            else   
            {       
                cout << "空";   
            }    
        }  
    }
private   
    std::list<int> msgRecvQueue;			// 收到的消息列表  
    std::mutex mymutex;						// 【互斥量】创建一个互斥量成员变量
}  
int main()
{   
    A a;  
    thread threadI(&A::inMsgRecQueue, &a);	// 成员函数作为线程初始函数 
    thread threadO(&A::outMsgRecQueue, &a);	// 成员函数作为线程初始函数 
    threadI.json();
    threadO.json(); 
    cout << "《主线程》" << endl    
        <<"《线程id》"<<std::this_thread:get_id()<<endl;  
    return 0;
}unique_lock 休眠操作
休眠(锁完以后休眠20s)
std::unique_lock<std::mutex> sbguard(my_mutex);	// 【unique_lock】
std::chrono::milliseconds dura(2000);			// 【20s】
std::this_thread::sleep_for(dura);				// 【休息20s】unique_lock 第二个参数
第二个参数:该参数是一个结构体对象,起一个标记的作用
std::adopt_lock作用:表示这个互斥量已经lock()了,不需要再lock一遍,只要负责析构时解锁就行了
使用前需要手动lock互斥量应用:创建智能互斥量对象(组合)
std::lock(mutexA, mutexB); // 先组合锁 std::unique_lock<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::unique_lock<std::mutex> sbguard(mutexB, std::adopt_lock);这个标记和在lock_guard中的用法一样
std::try_to_lock作用:尝试去lock(),若没有锁定成功则立即返回,而不会阻塞
注意使用前不要自己先lock互斥量,否则会出错应用:
std::unique_lock<std::mutex> sbgruad1(my_mutex1, std::try_to_back); if(shguard1.owns_lock()) // 拿到了锁 { // 操作共享数据 } else // 没拿到锁 { // 干点其他事情 }
std::defer_lock作用:初始化一个没有加锁的mutex
使用前不能自己lock互斥量,否则报异常
使用后需要lock(),且不需要手动解锁优点:加锁解锁较自由
缺点:这里并没有真正加锁原来的那个互斥量,读写时可能会出问题(不稳定)
应用:
std::unique_lock<std::mutex> sbgruad1(my_mutex1, std::defer_lock); // 初始化一个没有加锁的mutex并添加到智能指针的成员 subgruad1.lock(); // 加锁那个初始化的mutex,并且该mutex可以被自动解锁 // 处理一些非共享数据 subgruad1.unlock(); subgruad1.lock(); // 继续处理共享数据 // subgruad1.unlock(); // 写不写都行,该模式的智能互斥量指针析构会自动检查有无解锁
unique_lock 成员函数
lock()- 加锁,和互斥量用法一样
 
unlock()- 解锁,和互斥量用法一样
 
try_lock()- 尝试给互斥量加锁,如果拿不到锁则返回false,否则返回true。不阻塞
 - 和unique_lock的
std::try_to_lock参数的作用类似 
release()返回它管理的mutext对象指针,并释放所有权
即使unique_lock和构建它的mutext不再有关系
std::unique_lock<std::mutex> sbguard1(my_mutex1); std::mutex *ptx = sbguard1.release(); // 此时 ptx == my_mutex1。由于和智能互斥量解除关系,现在需要自己解锁了 // ... ptx->unlock();
unique_lock 所有权的传递
概念
如下例程所示
std::unique_lock<std::mutex> sbguard1(mutex1); // 此时sbguard1拥有mutex1的所有权 std::unique_lock<std::mutex> sbguard2(mutex1); // 此时sbguard2不能再拥有mutex1的所有权,该行会报错
所有权的转移
概念:unique_lock该对象可以转移mutex参数的所有权,但不能复制。这和unique_ptr一样,属于独占型智能指针
方法1(std::move方法调用移动构造函数)
std::unique_lock<std::mutex> sbguard1(mutex1); // 此时sbguard1拥有mutex1的所有权 // std::unique_lock<std::mutex> sbguard2(sbguard1); // 非法报错,不能复制所有权 std::unique_lock<std::mutex> sbguard2(std:move(sbguard1)); // 移动语义,参数变为右值,所有权转移。此时sbguard1指向空方法2(返回局部对象从而调用移动构造函数)
std::unique_lock<std::mutex> rtn_unique_lock(){ std::unique_lockKstd::mutex> tmpguard(my_mutex1): return tmpguard: //从函数返回一个局部的unique_lock对象是可以的。 // 移动构造函数的知识:返回局部对象会导致系统生成临时的对象,并调用相应的移动构造函数 } std::unique_lock<std::mutex> sbguard1 = rtn_unique_lock();
死锁(多个互斥量)
造成原因:至少有两个互斥量才会造成死锁
- 抽象场景举例 
- 线程1依次执行:
mutexA.lock();、mutexB.lock(); - 线程2依次执行:
mutexB.lock();、mutexA.lock(); - 当线程1锁完mutexA后上下文切换为线程2并锁了mutexB,就会形成死锁
 - 此时两个线程都会一直阻塞
 
 - 线程1依次执行:
 - 具体场景距离 
- 占有资源、并去抢占其他资源,就有可能会出现死锁
 
 
- 抽象场景举例 
 解决方案
- 保证两个互斥量上锁的顺序一致
 - 可用
std::lock()函数模板解决 
std::lock()函数模板- 作用:一次锁住多个互斥量,可避免在多个线程中因为锁顺序问题导致死锁
 - 原理:如遇到没被锁的互斥量则锁住,直到互斥量中有一个没锁住则立即解锁之前锁住的互斥量,并阻塞循环直到组合中所有互斥量都被锁住
 - 使用:
std::lock(mutexA, mutexB);后,要与解锁配对:mutexA.unlock(); mutexB.unlock(); 
结合智能互斥量
std::adopt_lock参数- 简概:这是个结构体对象,起一个标记的作用
 - 作用:表示这个互斥量已经lock()了,不需要再lock一遍,只要负责析构时解锁就行了
 - 应用:创建智能互斥量对象(组合)
 
写法
std::lock(mutexA, mutexB); // 先组合锁 std::lock_guard<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::lock_guard<std::mutex> sbguard(mutexB, std::adopt_lock);
总结
std::mutex,创建互斥量对象std::lock(),组合锁的函数模板创建智能互斥量对象(非组合)
std::lock_guard<std::mutex> sbguard(mutexA);
创建智能互斥量对象(组合)
std::lock(mutexA, mutexB); // 先组合锁 std::lock_guard<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::lock_guard<std::mutex> sbguard(mutexB, std::adopt_lock);