多线程编程lock_guard 和unique_lock (第六讲)

平时看代码时,也会使用到std::lock_guard,但是std::unique_lock用的比较少。

在看并发编程,这里总结一下。方便后续使用。

std::unique_lock也可以提供自动加锁、解锁功能,比std::lock_guard更加灵活。

std::lock_guard

std::lock_guard是RAII模板类的简单实现,功能简单。

1.std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁。
2.锁在多线程编程中,使用较多,因此c++11提供了lock_guard模板类;在实际编程中,我们也可以根据自己的场景编写resource_guard RAII类,避免忘掉释放资源。

下面是一个使用std::lock_guard的代码例子,1+2+ .. + 100的多线程实现,每个num只能由一个线程处理。:

#include <thread>
#include <mutex>
#include <vector>
#include <iostream>
#include <algorithm>

std::mutex my_lock;

void add(int &num, int &sum){
    while(true){
        std::lock_guard<std::mutex> lock(my_lock);  
        if (num < 100){ //运行条件
            num += 1;
            sum += num;
        }   
        else {  //退出条件
            break;
        }   
    }   
}

int main(){
    int sum = 0;
    int num = 0;
    std::vector<std::thread> ver;   //保存线程的vector
    for(int i = 0; i < 20; ++i){
        std::thread t = std::thread(add, std::ref(num), std::ref(sum));
        ver.emplace_back(std::move(t)); //保存线程
    }   

    std::for_each(ver.begin(), ver.end(), std::mem_fn(&std::thread::join)); //join
    std::cout << sum << std::endl;
}

std::unique_lock

类 unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用
unique_lock比lock_guard使用更加灵活,功能更加强大。
使用unique_lock需要付出更多的时间、性能成本。

下面是try_lock的使用例子。

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock
#include <vector>

std::mutex mtx;           // mutex for critical section
std::once_flag flag;

void print_block (int n, char c) {
    //unique_lock有多组构造函数, 这里std::defer_lock不设置锁状态
    std::unique_lock<std::mutex> my_lock (mtx, std::defer_lock);
    //尝试加锁, 如果加锁成功则执行
    //(适合定时执行一个job的场景, 一个线程执行就可以, 可以用更新时间戳辅助)
    if(my_lock.try_lock()){
        for (int i=0; i<n; ++i)
            std::cout << c;
        std::cout << '\n';
    }
}

void run_one(int &n){
    std::call_once(flag, [&n]{n=n+1;}); //只执行一次, 适合延迟加载; 多线程static变量情况
}

int main ()
{
    std::vector<std::thread> ver;
    int num = 0;
    for (auto i = 0; i < 10; ++i){
        ver.emplace_back(print_block,50,'*');
        ver.emplace_back(run_one, std::ref(num));
    }

    for (auto &t : ver){
        t.join();
    }
    std::cout << num << std::endl;
    return 0;
}

 

class A
{
    public:
    std::unique_lock<std::mutex> itsUnique_lock()
    {
          std::unique_lock<std::mutex> tempUnique_lock(my_mutex1);
          return  tempUnique_lock;
          //从函数返回一个局部的unique_lock对象是可以的。
          //返回这种局部对象,tempUnique_lock会调用unique_lock的移动构造函数
          //在下面可以这样调用:
          //std::unique_lock<std::mutex> sbguard1 = itsUnique_lock();
          //sbguard1 就有了my_mutex1的所有权
    } 

    void inMsgRecvQueue()    //unlock()
    {
        for(int i=0;i<10000;++i)
        {
            cout<<"inMsgRecvQueue():"<<i<<endl;
            my_mutex.lock();
            std::unique_lock<std::mutex> lguard(my_mutex,std::adopt_lock);
            //使用std::adopt_lock的前提条件是my_mutex已经加锁成功了                             
            //my_mutex.lock();下面一行是try_to_lock所以千万不能提前加锁,不然会为一个mutex对象加两次锁
            std::unique_lock<std::mutex> lguard(my_mutex,std::try_to_lock); 
                  //使用std::adopt_lock的前提条件是my_mutex已经加锁成功了
            if(lguard.owns_lock())
            {
                msgRecvQueue.push_back(i);//拿到了锁
            }else
            {
                //没拿到锁
                cout<<"inMsgRecvQueue()执行:但没有加锁成功,干点别的事"<<endl;
            }
            //这个可以不用管unlock(),它自动调用my_mutex.unlock()   
            //std::lock_guard<std::mutex> lguard(my_mutex);  
            
            //std::lock_guard<std::mutex> lguard1(my_mutex1); 
            //std::lock_guard<std::mutex> lguard2(my_mutex2); 
            
            std::lock(my_mutex1,my_mutex2);//相当于每个互斥量都调用了lock(),下面的2个unlock()必须还要写    
            std::lock_guard<std::mutex> lguard1(my_mutex1,std::adopt_lock);
            //多了后面一个参数std::adopt_lock就可以不调用构造函数里mutex::lock()
            std::lock_guard<std::mutex> lguard2(my_mutex2,std::adopt_lock); 
            //而且使用了lock_guard就不需要在后面写unlock()了
            std::lock(my_mutex1,my_mutex2);
            //相当于每个互斥量都调用了lock(),下面的2个unlock()必须还要写
            //my_mutex.lock();
            my_mutex1.lock();
            my_mutex2.lock();
            msgRecvQueue.push_back(i);
            //my_mutex.unlock();
            my_mutex1.unlock();
            my_mutex2.unlock();
        }
    
    }
    
    bool outMsgProc(int &command)
    {
        
        std::unique_lock<std::mutex> lguard(my_mutex);
        std::lock_guard<std::mutex> lguard(my_mutex1)//lguard是对象名
        //lock_guard构造函数里执行了mutex::lock()
        //lock_guard析构函数里执行了mutex::unlock()            
        //std::lock_guard<std::mutex> lguard1(my_mutex1); 
        //std::lock_guard<std::mutex> lguard2(my_mutex2); 
        std::lock(my_mutex1,my_mutex2);
        //相当于每个互斥量都调用了lock(),下面的2个unlock()必须还要写 
   
        std::lock_guard<std::mutex> lguard1(my_mutex1,std::adopt_lock); 
        //多了后面一个参数std::adopt_lock就可以不调用构造函数里mutex::lock()

        std::lock_guard<std::mutex> lguard2(my_mutex2,std::adopt_lock); 
        //使用std::adopt_lock的前提条件是my_mutex2已经加锁成功了
        
        
        //my_mutex.lock();
        //my_mutex1.lock();
        //my_mutex2.lock();
        
        
        std::chrono::milliseconds dura(20000);//一秒=1000,所以这是20秒
        std::this_thread::sleep_for(dura);//休息一定时间
        if(!msgRecvQueue.empty())
        {
            command =msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在   
            msgRecvQueue.pop_front();//移除第一个元素
            //my_mutex.unlock();
            my_mutex1.unlock();
            my_mutex2.unlock();
            return true;
        }
            
            //my_mutex.unlock();
            my_mutex1.unlock();//lock 的顺序需要一致
            my_mutex2.unlock();
            return false;        
    }
    //遇到共享数据段的操作,就先lock() ,操作完了后,再unlock()
    void outMsgRecvQueue()
    {
        int command =0;
        for(int i=0;i<10000;++i)
        {
            bool result=outMsgProc(command);
            if(result==true)
            {
                cout<<"outMsgRecvQueue()执行,取出一个元素"<<command<<endl;
            }
            else
            {
                //消息队列为空
                cout<<"outMsgRecvQueue()执行,但是消息队列为空"<<i<<endl;
            }
        }
    }
    
    private:
    std::list<int> msgRecvQueue;//容器(消息队列)专门用于代表玩家发送的命令
    std::mutex my_mutex1;//创建了一个互斥量
    std::mutex my_mutex2;//创建了一个互斥量
    
    
};

int main()
{
    A myobja;
    std::thread my_in_Thread(&A::outMsgRecvQueue,&myobja);
    //第二个参数是引用,才能保证线程里,用的是同一个对象,不然会调用拷贝构造,进行深拷贝
    std::thread my_out_Thread(&A::inMsgRecvQueue,&myobja);
    
    outMsgRecvQueue.join();
    inMsgRecvQueue.join();
    //使用unique_lock(类模板)取代lock_guard,unique_lock(比ock_guard更灵活,但是效率降差一点,内存占用多一点
    //工作中一般lock_guard是可以的。但是有些特殊需求会用到unique_lock
    //unique_lock的第二个参数
    //lock_guard的第二个参数
    //std::lock_guard<std::mutex> lguard1(my_mutex1,std::adopt_lock);//adopt_lock起标记作用
    //2.1 std::adopt_lock::表示互斥量已经lock了,(你必须要把互斥量提前lock了,否则会报异常)
    //std::adopt_lock标记的效果就是“假设调用的线程已经拥有量互斥量的使用权(已经lock成功了)”
    //unique_lock也可以带std::adopt_lock这个标记,含义:就是不希望在unique_lock的构造函数中调用mutex的lock()了
    
    cout<<"主线程收尾,正常退出"<endl;
    return 0;
}


//保护共享数据,操作时,某个线程用代码把共享数据锁住,操作数据解锁。
其他想操作共享数据的线程必须等待解锁,锁定住,操作,解锁。
互斥量:mutex
互斥量是一个类,理解成一把锁,多个线程尝试用lock()成员函数来加锁。只有一个线程可以锁定成功,(成功标志这个lock()成功返回了)
如果没加锁成功,那么流程就卡在lock()这里,直到加锁成功。
互斥量使用要小心,保护数据不多也不少。多了影响效率,少了起不到效果。

需要加入头文件  #include <mutex>

互斥量的用法:

2.1 lock()  ,unlock()  这两个必须成对使用,而且不允许调用两次lock()或者unlock()
   步骤:先 lock() 然后操作共享数据, 再unlock()

用了lock() 但是忘记了unlock()的问题,有时候特别难排查,if的条件可能在特殊条件下才成立。

未来防止忘记unlock(),引入了std::lock_guard的类模板:它会替你unlock()

2.2  std::lock_guard类模板:直接取代lock()  ,unlock() 

      std::lock_guard<std::mutex> lguard(my_mutex1)      //lguard是对象名

三:死锁:

两个线程A、B
(a)线程A执行的时候,这个线程先把a锁lock()成功,然后去锁b锁。线程A拿不到b锁就停在这里一直加锁
出现上下文切换
(b)线程B执行的时候,这个线程先把b锁lock()成功,然后去锁a锁。线程B拿不到a锁就停在这里一直加锁
这时死锁就产生了。
死锁的解决方案:两个互斥量的调用顺序必须保持一致

std::lock()函数模板      如果互斥量中有一个没锁住,它就在那里等,所有的锁都锁住才会往下走。
std::lock()作用:一次锁住两个或两个以上互斥量(至少两个,多了不限,1个不行)它不存在这种因为在多线程中因为锁的顺序问题导致的死锁问题,它就在那里等着,等所有互斥量都锁住才会继续往下走(返回)
要么互斥量都锁住,要么互斥量都没锁住,如果只锁一个,其他的没锁住,就会放开锁住的互斥量。

std::lock_guard的std::adopt_lock参数是结构体对象,作用:表示这个互斥量已经被lock()过了,
不需要在std::lock_guard<std::mutex> 构造函数里面对mutex对象进行lock()了。
std::lock_guard<std::mutex> lguard(my_mutex1)//lguard是对象名
                              //lock_guard构造函数里执行了mutex::lock()
                            //lock_guard析构函数里执行了mutex::unlock()

std::lock_guard<std::mutex>    可以取代    mutex::lock()和    mutex::unlock() 

               
std::lock_guard<std::mutex> lguard1(my_mutex1,std::adopt_lock);


 多了后面一个参数std::adopt_lock就可以不调用构造函数里mutex::lock(),前面的代码已经lock()过了。

小结:std::lock()可以一次锁多个互斥量,如果有没锁住的就会放开。


并发与多线程 unique_lock详解

(1)unique_lock取代lock_guard
(2)unique_lock的第二参数  std::adopt_lock   前面必须已经加锁了
                                                std::try_to_lock    前面必须还没有被加锁
                                                std::defer_lock

(3)unique_lock()的成员函数
        3.1 lock()   加锁
        3.2 unlock()   解锁 

 //因为lock锁住的代码段越少,执行越快,有的人把锁住的代码多少,称为锁的粒度,一般用粗细来描述。

锁住的代码少,这个粒度叫细,执行效率高。

锁住的代码多,这个粒度叫粗,执行效率低。

要学会选择合适的粒度来保护代码。
        3.3 try_lock()  尝试给互斥量加锁,如果没加成功返回false 如果加锁成功返回true
        3.4 release()   返回它管理的mutex指针,并释放所有权,也就是说unique_lock()和mutex不再有关系
严格区分unlock()和release(),不要混淆。如果原来mutex对象处于加锁状态,你使用了 release(),那么你有责任,负责解锁。
(4)unique_lock所有权的传递  

unique_lock是和mutex绑定到一起的,unique_lock需要管理一个mutex对象

一个mutex对象只能与一个unique_lock绑定到一起。


std::try_to_lock


      我们会尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里
用try_to_lock的前提是你不能先去lock


defer_lock的意思,就是没有给mutext加锁,初始化了一个没有加锁的mutex

我们接着std::defer_lock的话题来介绍unique_lock的重要成员函数

             
            std::unique_lock<std::mutex> lguard1(my_mutex1,std::defer_lock); 

                                                            std::defer_lock  前提是你自己不能先加锁,否则会报异常

                                                            defer_lock的意思,就是没有给mutext加锁,初始化了一个没有加锁的mutex

            //lguard1对象拥有my_mutex1对象的所有权
            lguard1.lock(); //不用自己unlock
            //操作共享代码
            lguard1.unlock(); 
            //处理一些非共享代码
            lguard1.lock(); 
            //操作共享代码
            msgRecvQueue.push_back(i);
            std::unique_lock<std::mutex> lguard1(my_mutex1,std::defer_lock); 
            lguard1.try_to_lock(); 
            msgRecvQueue.push_back(i);
             std::unique_lock<std::mutex> lguard1(my_mutex1);

             //unique_lock可以自动加锁,离开作用域时会检查有没有加锁,如果加了它会自动解锁,

            也可以通过成员函数做灵活的加锁和解锁
            std::mutex *p = lguard1.release();//使用了release后返回的是mutex的指针,现在你有责任自己解锁
            msgRecvQueue.push_back(i);
            p->unlock();//release后需要自己负责mutex的解锁

//转移所属权

1)使用std::move

2)return std::unique_lock<std::mutex>

std::unique_lock<std::mutex> lguard1(my_mutex1);

std::unique_lock<std::mutex> lguard2(std::move(lguard1));

//移动语义,现在相当于lguard2和my_mutex1绑定到一起了,lguard1指向空

std::move()//可以使左值变右值,会调用移动构造函数。

  std::unique_lock<std::mutex> itsUnique_lock()
    {
          std::unique_lock<std::mutex> tempUnique_lock(my_mutex1);
          return  tempUnique_lock;
          //从函数返回一个局部的unique_lock对象是可以的。
          //返回这种局部对象,tempUnique_lock会调用unique_lock的移动构造函数
     //在下面可以这样调用:
      //std::unique_lock<std::mutex> sbguard1 = itsUnique_lock();
                    //sbguard1 就有了my_mutex1的所有权

    } 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aFakeProgramer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值