跳至主要內容

多线程 - 线程锁

LincZero大约 9 分钟

多线程 - 线程锁

互斥量(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,就会形成死锁
      • 此时两个线程都会一直阻塞
    • 具体场景距离
      • 占有资源、并去抢占其他资源,就有可能会出现死锁
  • 解决方案

    • 保证两个互斥量上锁的顺序一致
    • 可用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);