zoukankan      html  css  js  c++  java
  • C++ 多线程 (4) 互斥量(mutex)与锁(lock)

    @

    一、基本概念

    在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题。因此就需要引入锁的机制,来保证任意时候只有一个线程在访问公共资源。

    • 互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
    • 互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。

    二、使用方法

    包含头文件#include <mutex>

    2.1 mutex.lock(),unlock()

    步骤:1.lock(),2.操作共享数据,3.unlock()。
    lock()和unlock()要成对使用,不能重复上锁和解锁。本质就是lock~unlock之间的程序(数据)不会同时调用、修改。

    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <list>
    using namespace std;
    
    list<int> test_list;
    
    mutex my_mutex;
    void in_list(){
        for(int num=0;num<1000000000000000000;num++){
            my_mutex.lock();
            cout<<"插入数据: "<<num<<endl;
            test_list.push_back(num);
            my_mutex.unlock();
        }
    
    }
    
    void out_list(){
    
        for(int num=0;num<1000000000000000000; ++num){
            my_mutex.lock();
            if(!test_list.empty()){
                int tmp = test_list.front();
                test_list.pop_front();
                cout<<"取出数据:"<<tmp<<endl;
    
            }
            my_mutex.unlock();
    
        }
    }
    int main()
    {
    
    
        thread in_thread(in_list);
        thread out_thread(out_list);
        in_thread.join();
        out_thread.join();
        cout << "Hello World!" << endl;
    
        return 0;
    }
    
    

    2.2 std::lock_guard类模板

    lock_guard构造函数执行了mutex::lock(),在作用域结束时,自动调用析构函数,执行mutex::unlock()

    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <list>
    using namespace std;
    
    list<int> test_list;
    
    mutex my_mutex;
    void in_list(){
        for(int num=0;num<1000000000000000000;num++){
            std::lock_guard<std::mutex> my_guard(my_mutex);
            cout<<"插入数据: "<<num<<endl;
            test_list.push_back(num);
    
        }
    
    }
    
    void out_list(){
    
        for(int num=0;num<1000000000000000000; ++num){
            std::lock_guard<std::mutex> my_guard(my_mutex);
            if(!test_list.empty()){
                int tmp = test_list.front();
                test_list.pop_front();
                cout<<"取出数据:"<<tmp<<endl;
    
            }
         
    
        }
    }
    int main()
    {
    
    
        thread in_thread(in_list);
        thread out_thread(out_list);
        in_thread.join();
        out_thread.join();
        cout << "Hello World!" << endl;
    
        return 0;
    }
    
    

    2.2.1 std::lock_guard的std::adopt_lock参数

    std::lock_guard<std::mutex> my_guard(my_mutex,std::adopt_lock);
    

    加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
    adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。

    2.3 std::unique_lock函数模板

    unique_lock想比于lock_guard,都是基于RAII思想的,也支持std::lock_guard的功能,但是区别在于它提供更多的成员函数,比如:lock(),unlock()使用更加灵活,并且可以和condiction_variable一起使用控制线程同步。但是效率差一点,内存占用多一点。

    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <list>
    using namespace std;
    
    list<int> test_list;
    
    mutex my_mutex;
    void in_list(){
        for(int num=0;num<1000000000000000000;num++){
            std::unique_lock<std::mutex> my_guard(my_mutex);
            cout<<"插入数据: "<<num<<endl;
            test_list.push_back(num);
    
        }
    
    }
    
    void out_list(){
    
        for(int num=0;num<1000000000000000000; ++num){
            std::unique_lock<std::mutex> my_guard(my_mutex);
            if(!test_list.empty()){
                int tmp = test_list.front();
                test_list.pop_front();
                cout<<"取出数据:"<<tmp<<endl;
    
            }
         
    
        }
    }
    int main()
    {
    
    
        thread in_thread(in_list);
        thread out_thread(out_list);
        in_thread.join();
        out_thread.join();
        cout << "Hello World!" << endl;
    
        return 0;
    }
    
    
    

    2.3.1 unique_lock的第二个参数

    1) std::adopt_lock:

    - 表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
    - 前提:必须提前lock
    - lock_guard中也可以用这个参数
    

    2) std::try_to_lock:

    • 尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,但也不能操作保护的数据(防止异常),只能操作不受保护的数据;
    • 使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
    • 前提:不能提前lock();
    • unique_lock.owns_locks()方法判断是否拿到锁,如拿到返回true
    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <list>
    using namespace std;
    
    list<int> test_list;
    
    mutex my_mutex;
    void in_list(){
        for(int num=0;num<10000;num++){
            std::unique_lock<std::mutex> my_unique(my_mutex, std::try_to_lock);
            if(my_unique.owns_lock()){
                cout<<"插入数据: "<<num<<endl;
                test_list.push_back(num);
            }
            else{
                cout<<"没能拿到锁,只能干点别的事"<<endl;
            }
    
    
        }
    
    }
    
    void out_list(){
    
        for(int num=0;num<10000; ++num){
    
    
            std::unique_lock<std::mutex> my_unique(my_mutex);
            std::chrono::seconds dura(1);
            std::this_thread::sleep_for(dura);
            if(!test_list.empty()){
                int tmp = test_list.front();
                test_list.pop_front();
                cout<<"取出数据:"<<tmp<<endl;
    
            }
            else {
                cout<<"已经空了"<<endl;
            }
    
    
        }
    }
    int main()
    {
    
    
        thread in_thread(in_list);
        thread out_thread(out_list);
        in_thread.join();
        out_thread.join();
        cout << "Hello World!" << endl;
    
        return 0;
    }
    
    

    3) std::defer_lock:

    • 加上defer_lock是始化了一个没有加锁的mutex
    • 不给它加锁的目的是以后可以调用后面提到的unique_lock的一些方法
    • 前提:不能提前lock

    2.3.2 unique_lock的成员函数

    							(前三个与std::defer_lock联合使用)
    

    1)lock():加锁

    unique_lock<mutex> myUniLock(myMutex, defer_lock);
    myUniLock.lock();
    

    作用是:不用自己unlock();
    2)unlock():解锁。

    unique_lock<mutex> myUniLock(myMutex, defer_lock);
    myUniLock.lock();
    //处理一些共享代码
    myUniLock.unlock();
    //处理一些非共享代码
    myUniLock.lock();
    //处理一些共享代码
    

    作用:因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()。
    3)try_lock():尝试给互斥量加锁
    如果拿不到锁,返回false,否则返回true。用法和前面的try_to_lock参数一致。
    4)release():释放unique_lock所管理的mutex对象指针

    • myUniLock(myMutex)相当于把myMutex和myUniLock绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权
    • 用法 mutex* ptx =myUniLock.release()
      所有权由ptx接管,如果原来mutex对象处理加锁状态,就需要自己进行解锁了。ptx->unlock();

    2.3.3 所有权转移

    1. 使用move转移
      unique_lock myUniLock(myMutex);把myMutex和myUniLock绑定在了一起,也就是myUniLock拥有myMutex的所有权
    // myUniLock拥有myMutex的所有权,myUniLock可以把自己对myMutex的所有权转移,但是不能复制。
    unique_lock myUniLock2(std::move(myUniLock));
    //现在myUniLock2拥有myMutex的所有权。
    
    1. 在函数中return一个临时变量,即可以实现转移
    unique_lock<mutex> aFunction()
    {
        unique_lock<mutex> myUniLock(myMutex);
        //移动构造函数那里讲从函数返回一个局部的unique_lock对象是可以的
        //返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
        return myUniLock;
    }
    

    2.4 std::lock()函数模板

    • std::lock(mutex1,mutex2……):一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。
      如果有一个没锁住,就会把已经锁住的释放掉,然后它就等待,等所有互斥量都可以同时锁住,才继续执行。(要么互斥量都锁住,要么都没锁住,防止死锁)

    三、死锁

    3.1 发生原因

    死锁至少有两个互斥量mutex1,mutex2。

    1. 线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
    2. 线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
    3. 此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。。。

    3.2 解决办法

    只要保证多个互斥量上锁的顺序一样就不会造成死锁。

    四、锁的效率

    lock的代码段越少,执行越快,整个程序的运行效率越高。

    1. 锁住的代码少,叫做粒度细,执行效率高;
    2. 锁住的代码多,叫做粒度粗,执行效率低;
  • 相关阅读:
    无规矩不成方圆,聊一聊 Spring Boot 中 RESTful 接口设计规范
    一次SQL查询优化原理分析(900W+数据,从17s到300ms)
    重磅!GitHub官方开源新命令行工具
    JVM调优的反思与总结
    SpringMVC 进阶版
    《四大点,搞懂Redis到底快在哪里?》
    《Docker基础与实战,看这一篇就够了》
    带你从头到尾捋一遍MySQL索引结构
    MySQL信息提示不是英文问题
    完美解决windows+ngnix+phpcgi自动退出的问题
  • 原文地址:https://www.cnblogs.com/long5683/p/12997011.html
Copyright © 2011-2022 走看看