• c++_learning-并发与多线程


    并发与多线程

    并发:

    一个程序(单核CPU)通过操作系统的调度进行“任务切换”,从而同时执行多个独立的任务,即提高性能,可以同时干多个事,但切换需要时间的开销。

    进程:

    就是一个可执行程序运行起来了 windows(双击.exe文件)/linux(./文件名)。一个进程中有一个主线程,用来执行main函数中的代码。

    线程:

    基本概念:

    • 除了主线程之外,可以通过写代码自己创建线程,每创建一个线程就多干一件事。
    • 切换需要耗费时间的成本,所以并不是线程越多越好。
    • 每个线程都需要一个独立的堆栈空间(1M),线程之间的切换需要保存很多中间状态。线程结束后,需要回收该线程的这些资源。

    线程安全:

    问题出现的场景:

    • 同一个进程中的多个线程共享该进程的全部系统资源。
    • 多个线程访问同一个共享资源时,会产生冲突。

    涉及的性质:

    顺序性:顺序执行代码。

    • 存在的问题:为了提高程序运行效率,CPU可能会对代码进行优化,按更高效的顺序执行代码,而不一定是顺序执行,但执行结果与顺序执行的结果是一致的。

    可见性:当多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程能够立即看到。

    • 存在的问题:线程操作共享变量时,会将该变量从内存加载到CPU的缓存中,修改该变量后CPU会立即更新缓存,但不一定会立即将它写回内存中。此时,其他线程访问该变量,是从内存中得到的旧数据,而非第一个线程操作后的数据。

    原子性:一个操作(可能包含多个步骤)要么全部执行,要么全部不执行。

    • 存在的问题:CPU执行“读取指令、读取内存、执行指令、写回内存”的过程中,并不是一气呵成的,可能会被打断。

    如何保证线程安全?

    1. volatile关键字:保证内存变量的可见性禁止代码优化(重排序)
    2. 原子操作(原子类型)。
    3. 线程同步(互斥锁)。

    并发的实现手段(优先使用多线程并发):

    多进程并发中,进程之间的通信IPC:

    1. 同一台电脑:管道(有名管道、无名管道)、文件、消息队列、共享内存。
    2. 不同的电脑:socket通信技术,即本地套接字。

    多线程并发,单进程中创建多个线程来实现并发:

    • 每一个进程中的所有线程共享地址空间(共享内存)(全局变量、指针、引用,都可以在线程之间传递;且开销比较小)。
    • 共享内存带来的问题:数据一致性的问题。

    线程的启动、结束和创建多线程的方法:

    • 主线程是从main()函数开始执行的;子线程的执行也必须从一个函数开始;

    • 当一个进程中,主线程执行结束后,如果子线程还没有结束会被操作系统强制终止。如果要保证子线程的运行状态,就必须保证主线程的运行状态,或者使用detach使子线程处于“分离状态”。

    • c++11标准线程库#include,提高了移植性。其中,thread是一个标准库中的类,使用时要创建类对象。

      // 使用时,只是引用了地址并未发生拷贝动作
      std::ref()   // 给线程函数传递参数时,可通过此函数避免发生拷贝动作
      
      /* 构造函数 */
      // 1.默认构造函数,构造一个线程对象且不执行任何任务:
      thread() noexcept;
      
      // 2.创建线程对象,并在线程中执行任务函数
      template<class Function, class... Args>
      explicit thread(Function&& fx, Args&&... args);    // fx是任务函数,args是任务函数执行时需要的参数
      // 任务函数可以是普通函数、类的静态成员函数、类的非静态成员函数、lambda表达式、仿函数
      
      // 3.删除了拷贝构造函数,不允许线程对象之间的拷贝
      thread(const thread&)=delete
      
      // 4.移动构造函数,将线程other的资源所有权转移给新创建的线程对象
      thread(thread&& other) noexcept
      
      /* 赋值函数 */
      // 左值的other禁止拷贝,故删除该赋值函数
      thread& operator=(const thread& other)=delete
      // 右值的other能赋值,会发生资源所有权的转移
      thread& operator=(const thread&& other) noexcept
      
      /* 阻塞主线程,并等待子线程执行完毕后回收它的资源,然后子线程与主线程汇合一起执行其他程序 */ 
      join() 
      /* 主线程不用等待子线程结束;一旦detach后,该子线程会被c++运行时库接管,运行结束后,由运行时库负责清理该线程相关的资源 */
      detach()/* 判断子线程的分离状态并返回布尔类型,即是否可以成功使用join()、detach()(true则是可以使用) */
      joinable()/* c++11中,命名空间std::this_thread的全局函数 */
      this_thread::get_id()          // 得到线程的id
      this_thread::sleep_for()       // 使线程休眠一段时间
          // 线程休眠1秒
      	Sleep(1000);
      	this_thread::sleep_for(chrono::seconds(1));
      this_thread::sleep_until()     // 让线程休眠到某个时间点,可实现线程定时任务
      this_thread::yield()           // 让线程主动让出自己已抢到的CPU时间片
      
      /* thread类的其他成员函数 */
      swap(std::thread& other)                           // 交换两个线程对象
      static unsigned hardware_concurrency() noexcept    // 返回硬件线程上下文的数量
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
    • 其他创建线程的方法:

      用类:类中必须要重载()运算符,将该类对象变成可调用对象,才能用来初始化线程对象。其中涉及到的过程:有参构造 --> 拷贝构造函数 --> 析构函数,最后对有参构造的类对象进行析构。

      用lambda表达式,完成创建。

      auto mylambdathread = []() {
          cout << "子线程开始执行" << endl;
          // .....
          cout << "子线程执行结束" << endl;
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5

    创建线程的要点:

    给子线程入口函数传递参数时,要用值传递(不能用引用或者指针):

    1. 简单的类型参数要用,值传递,不要用引用。

    2. 如果传递类对象,则要避免隐式类型转换(即创建子线程时,就生成临时对象并拷贝给入口函数)且该入口函数的形参中类对象要用常量引用。

      // 该入口函数的形参中,类对象用常量引用
      myPrint(const int i, const string &mystring) {...}
      
      // 创建子线程时,就生成临时对象并拷贝给入口函数
      thread mythreadObj(myPrint, i, string(mychar));
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 整个过程发生在主线程中;
      • 会调用该类的(有参)构造函数和拷贝构造函数,和一个析构函数(析构的是(有参)构造函数);
      • 临时对象构造时机的捕获:通过std::this_thread:;get_id()得到,给子线程入口函数传递类对象时,直接进行类型转换,这会将产生的临时对象会拷贝给入口函数(整个过程都发生在主线程中)。

    只使用.join(),就不会存在局部变量失效,导致线程对内存的非法引用的问题。

    传递类对象、智能指针、类成员函数指针作为线程参数:

    1. 传递类对象时,如果希望在子线程中修改该类对象的成员,则要用std::ref()函数引用类对象的地址且不会发生拷贝动作。

    2. 智能指针unique_ptr作为线程参数:

      unique_ptr<int> i_uptr(new int(100));
      
      thread mythreadObj(myPrint, std::move(i_uptr));
      
      • 1
      • 2
      • 3
    3. 类成员函数指针:

      thread mythreadObj(&A::func, a, 10);
      
      thread mythreadObj(&A::func, &a, 10);   // &a == std::ref(a);
      
      • 1
      • 2
      • 3

    创建和等待多个线程:

    将多个线程对象放入thread数组:

    // 用thread数组创建多个线程
    thread mythreadObj[10];
    for (int i = 0; i < sizeof(mythreadObj) / sizeof(mythreadObj[0]); i++)
    {
        mythreadObj[i] = thread(myPrint1, i);
    }
    for (int i = 0; i < sizeof(mythreadObj) / sizeof(mythreadObj[0]); i++)
    {
    	mythreadObj[i].join();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    将多个线程对象放入容器:

    // 用vector容器创建多个线程
    vector<thread> threadVctor;
    // 创建10个线程,线程入口函数统一使用myPrint()
    for (int i = 0; i < 10; i++)
    {
    	threadVctor.push_back(thread(myPrint1, i));  // 创建thread对象,并发生了拷贝到了容器中
    }
    for (vector<thread>::iterator iter = threadVctor.begin(); iter != threadVctor.end(); iter++)
    {
    	(*iter).join();   // 等价于iter->join();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    互斥锁:

    c++11-14-17_内存管理(RAII)_多线程:详细分析“c++多线程从原理到线程池实现”。

    多个线程的数据共享:

    数据共享问题分析:

    • 1)只读的数据:是安全稳定的,不需要特殊的处理;
    • 2)有读有写:读的时候不能写,写的时候不能读;否则会发生混乱而报错;

    解决的方法:引入互斥量mutex,

    • 写的时候锁住不让它读;读的时候锁住不让它写;确保同一时间只能有一个线程操作该共享资源;
    • 某个线程把共享数据锁住、操作数据、解锁;其他操作共享数据的线程必须等待解锁,然后才能锁定、操作数据、解锁;

    c++11中提供四种互斥锁:

    mutex互斥锁:

    含有的成员函数:.lock().unlock().join().joinable().detach()

    timed_mutex带超时机制的互斥锁:

    含有的成员函数:.try_lock_for(时间长度).try_lock_until(时间点)

    recursive_mutex递归互斥锁:

    允许同一个线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。

    recursive_timed_mutex带超时机制的递归互斥锁

    互斥量mutex:

    互斥量可以理解为一把锁:
    • lock的代码段越少,执行的速度越快,整个程序的运行效率越高。

      lock的力度,需要掌控。力度越大,执行效率越低;力度越小,共享数据的能保护力度越低。

    • 线程持有锁的时间越长,程序运行效率越低。

    mutex类:
    .lock()        // 加锁
    /*
        互斥锁有锁定、未锁定两种状态:
        1)未锁定状态下,调用lock()的线程会得到互斥锁的所有权,并将其上锁;
        2)锁定状态下,调用lock()的线程会“阻塞等待”,直到互斥锁变成未锁定的状态;
        多个线程尝试用lock()成员函数进行加锁,且只有一个线程能加锁成功;没锁成功,则子线程会卡在这并不断尝试去锁这把锁头。
    */
    
    .unlock()     // 解锁:只有持有锁的线程才能解锁
    /* std::mutex::lock()、std::mutex::unlock(),必须成对使用 */
    
    .try_lock()   // 尝试加锁
    /*
        1)如果互斥锁是为未加锁的状态,则加锁成功,函数返回true
        2)如果互斥锁是锁定的状态,则加锁失败,函数立即返回false且“线程不会阻塞”
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    C++中的死锁:

    产生的条件:至少要有两把锁头,也就是两个互斥量才能产生。

    死锁的现象:当两个线程(都对两把锁进行锁和解锁的操作,锁的顺序相反)分别锁住了两个锁头,就会各自去寻找另一个锁头,从而产生了死锁。

    本质原因:两个线程上锁的顺序不同。

    死锁的解决方案:

    • 保证两个互斥量在两个线程中的,锁的顺序相同。

    • std::lock()函数模板:

      // 作用:一次能够锁住两个或者两个以上的互斥量。
      // 不存在多个线程中,锁的顺序问题导致死锁的问题。
      
      // 写法一:
      std::lock(m_mutex1, m_mutex2);
      m_mutex1.unlock(); m_mutex2.unlock();
      
      
      // 写法二:
      std::lock(m_mutex1, m_mutex2);
      
      std::lock_guard<std::mutex> lguard1(m_mutex1, std::adopt_lock);
      std::lock_guard<std::mutex> lguard2(m_mutex2, std::adopt_lock);   
      // 参数std::adopt_lock是结构体对象,起标记作用:作用就是表示该互斥量已经被lock(),不需要lock_guard再进行锁的操作,只需进行解锁
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

      std::lock():

      1)如果有一个互斥量没锁住,它就会在那里等,等待所有互斥量都被锁住之后,才能往下走

      2)要么两个互斥量都锁住了,要么都没锁住;如果只有一个锁住了,另一个没锁成功,则会解锁已经被锁住的锁

    • 建议一个线程中不要对两个互斥量同时上锁,最好一个一个锁;工作中,使用lock_guard足够了。

    unique_locklock_guard都是管理锁的辅助类:

    为防止mutex加锁后忘记unlock(),引出std::lock_guard()类模板:
    std::mutex m_mutex;
    std::lock_guard<std::mutex> lockguard(m_mutex);
    
    • 1
    • 2

    lock_guard()采用了RAII的思想:

    • 在其构造函数中,执行std::mutex::lock()
    • 在其析构函数中,执行std::mutex::unlock()

    缺点:lock_guard的解锁控制不灵活,但可以通过对lock_guard施加作用域来控制lock、unlock的生命周期。

    std::lock_guard<std::mutex> lguard(m_mutex, std::adopt_lock);
    // std::adopt_lock是结构体对象,起标记作用:表示该互斥量已经被lock(),不需要lock_guard再进行锁的操作,只需进行解锁
    
    • 1
    • 2
    类模板unique_lock取代lock_guard:
    unique_lock和lock_guard的异同点:
    • 区别:unique_lock占用的内存更多,效率低,但更加灵活。
    • 相同点:都是RAII风格,在构造函数中加锁,在析构函数中解锁。
    unique_lock第二个参数
    /* 1)std::adopt_lock是结构体对象,起标记作用:表示该互斥量已经被lock(),不需要unique_guard再进行锁的操作,只需进行解锁。*/
    
    
    /* 2)std::try_to_lock(使用前不能锁):会尝试用mutex的lock()去锁定这个mutex;但如果没有锁成功,则会立即返回,并不会阻塞在那里。*/
    std::unique_lock<std::mutex> lck(mtx, std::try_to_lock);
    
     // 判断互斥量是否拿到了锁
    bool succ = lck.owns_lock();
    
    
    /* 3)std::defer_lock:初始化未加锁的互斥量;但需要自己解锁。*/
    unique_lock<std::mutex> lck(mtx, std::defer_lock);  
     // 需要自己加锁;
    for (int i = 0; i < n; i++)
    {
        lck.lock();
        //操作共享数据
        //........
    
        lck.unlock();
    
        //操作非共享数据
        //........
    
        lck.lock();
        //操作共享数据
        //........
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    unique_lock的成员函数:
    lock()         // 加锁
    unlock()       // 解锁
    try_lock()     // 尝试加锁,拿到锁返回true,否则返回false且不会发生阻塞
    
    // release():
    /* 返回它所管理的mutex对象的指针(原始的mutex指针),并释放所有权(即unique_lock和mutex不再有关系了)*/
    unique_lock<std::mutex> lck(mtx);    
    mutex* pt_rel = lck.release();            
    // mtx所有权转让给pt_rel,因此需要pt_rel自己解锁
    
    //pt_rel->lock();    // Error:接收lck.release()无需再次上锁,即已上锁
    
    // .............操作共享数据
    
    pt_rel->unlock();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    unique_lock所有权的转移
    1. 通过std::move()进行所有权转移

      std::unique_lock<std::mutex> lck(mtx); 
      std::unique_lock<std::mutex> lck_move(std::move(lck));   // std::move()移动构造函数使用,转移所有权
      
      • 1
      • 2
    2. 通过函数返回局部的unique_lock对象(该局部对象,会生成临时对象并调用unique_lock的移动构造函数);

      std::mutex mtx;
      unique_lock<mutex> umtx_lockfunc()
      {
          unique_lock<mutex> temp_mtxLock(mtx); // temp_mtxLock拥有mtx的所有权,可将该互斥量mtx的所有权转移给其他的unique_lock对象(但不能复制)
          return temp_mtxLock;
      }
      
      // 通过函数umtx_lockfunc(),返回局部的unique_lock对象,生成了临时对象和调用了移动构造函数,从而将所有权转移给lock_move
      std::unique_lock<std::mutex> lck_move = umtx_lockfunc(); 
      
      // .......操作共享数据
      
      lck_move.unlock();
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    线程的移动和交换

    单例设计模式共享数据的问题:

    单类模式:确保多线程同时尝试创建一个的单类的实例时,只有一个能创建成功。提供一个访问它的全局访问点,该实例被所有程序模块共享。

    懒汉模式:

    单例实例在第一次被使用时才进行初始化,称为“延迟初始化”。

    • C++11前,多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:

      1)如果一个线程正在执行local static对象的初始化语句但还没有完成初始化;

      2)此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中,这会造成这个local static对象的重复构造,进而产生内存泄露问题。

    c++11引入std::call_once()的函数模板:保证函数只被调用一次;具备有互斥的能力,且效率高占用的互斥资源少。

    #include
    template<class callable, class...Args>
    void call_once(std::once_flag& flag, Function&& fx, Args&&... args); // fx(args...):函数名和参数
    // 当call_once调用成功后,once_flag对象会变成已调用的状态,后续就无法再次调用了;本质上once_flag是取值为0、1的锁。	
    
    • 1
    • 2
    • 3
    • 4
    #include 
    #include 
    #include 
    using namespace std;
    
    /* C++11线程安全 */
    class Singleton
    {
    public:
        shared_ptr<Singleton> getInstance()
        {
            // 该call_once中的函数或可调用对象一旦执行过一次,initFlag的状态就会改变。
            // 下次再调用时,会首先检查initFlag的状态,故不会再执行其中的函数或可调用对象。
            std::call_once(initFlag, [&this]() {
            	singleton = shared_ptr<Singleton>(new Singleton());
            });
            return singleton;
        }
    private:
        static shared_ptr<Singleton> singleton;
        // std::once_flag对象是一个不可复制、不可移动的对象,但可被默认构造。
        // 作用:跟踪call_once中,函数或可调用对象的是否已经被执行。
        static std::once_flag initFlag;
    
        Singleton() = default;
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    }; 
    static shared_ptr<Singleton> singleton = nullptr;
    static once_flag singletonFlag;  // 调用默认构造函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • C++11规定,在一个线程开始local static对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待。
    /* C++11线程安全 */
    class Singleton
    {
    private:
    	static Singleton* instance;
    private:
    	Singleton() {};
    	~Singleton() {};
    	Singleton(const Singleton&) = delete;
    	Singleton& operator=(const Singleton&) = delete;
    public:
    	static Singleton* getInstance() 
    	{
    		if(instance == nullptr) 
    		{
    			instance = new Singleton();
    		}
    		return instance;
    	}
    };
    
    // init static member
    Singleton* Singleton::instance = nullptr;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    “饿汉模式”线程安全:

    #include 
    using namespace std;
    
    class Singleton
    {
    public:
      // 获取单实例
      static Singleton* GetInstance()
      { 
          return singleton;
      } 
    
      // 释放单实例,进程退出时调用
      static void deleteInstance()
      {    
          if (singleton != nullptr)
          {
              delete singleton;
              singleton = nullptr;
          }
      }
    private:
      // 禁止外部调用默认构造和析构函数
      Singleton() {}
      ~Singleton() {}
    
      // 禁用拷贝构造和拷贝赋值函数
      Singleton(const Singleton &single);
      const Singleton &operator=(const Singleton &single);
    private:
      // 唯一单实例对象指针
      static Singleton* singleton;
    };
    // 代码一运行就初始化创建实例且创建一次,本身就线程安全
    Singleton* Singleton::singleton = new Singleton();
    
    int main()
    {
       Singleton::GetInstance();
       return 0;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    条件变量:std::condition_variable、wait()、notify_one():

    条件变量是一种线程同步机制:

    • 当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒
    • 举例:在两个线程中,一个线程向另一个线程发送消息,从而激活另一个线程

    常使用条件变量的场景:生产-消费者模型(高速缓存队列):

    为保护共享资源,“条件变量”需与“互斥锁”结合使用。。

    在这里插入图片描述

    注:生产者可以是单/多线程,而消费者一般都是多线程,俗称线程池。生产者产生的数据放入缓存队列,需要条件变量来通知消费者。

    class A2
    {
    public: 
        A2() { cout << "默认构造函数" << endl; }
        virtual ~A2() { cout << "虚析构函数" << endl; }
    
        // 将收集到的数据放入队列
        void InsertMessageQueue(int val)
        {
            for (int i = 0; i < 100000; i++)
            {
                std::unique_lock<std::mutex> umtx_lock (m_mutex);
                MessageQueue.push_back(val);
                m_conditionVar.notify_one();    // 尝试将另一个线程中的wait()唤醒
            }
        }
    
        void OutMessageQueue()
        {
            while (true)   // 死循环
            {
                std::unique_lock<std::mutex> umtx_lock(m_mutex);
                
                /* 循环阻塞当前线程,直到通知到达且谓词满足 */
                m_conditionVar.wait(umtx_lock, [this]() {
                    if (!this->MessageQueue.empty())
                    {	
                        return true;
                    }
                    return false;
                });
    
                // 流程能执行到这里,表明互斥量一直是锁着的状态;MessageQueue中至少有一条命令command 
                int retVal = MessageQueue.pop_back();
                return retVal;
            }    
        }
    public:
        list<int> MessageQueue;
        std::condition_variable m_conditionVar;
        std::mutex m_mutex;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    c++11的#include条件变量提供了两个类:

    condition_variable
    成员函数:

    只支持与普通mutex搭配,效率更高。

    condition_variable()   // 默认构造函数
    
    notify_one()           // 通知一个线程的wait函数
    notify_all()           // 同时通知多个线程的wait函数
    
    /*
        1)会把互斥锁解开;
        2)阻塞,并等待“被唤醒”且“满足谓词”;
        3)给互斥锁加锁;
        总结:wait导致当前线程阻塞直至“被条件变量通知”或“虚假唤醒发生”,可选地循环直至“满足某谓词”。
    */
    wait(unique_lock<mutex> lock)              // 阻塞当前线程,直到通知到达
    wait(unique_lock<mutex> lock, Pred pred)   // 循环阻塞当前线程,直到通知到达且谓词满足 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    当另一个线程的notify_one()函数将本线程中wait()函数激活后,

    ① wait()会尝试不断重新获取互斥量的锁,获取不到会卡在这里;获取到了(等于给互斥量加锁)则会继续执行 ②;

    ② 如果wait()函数有第二个参数,会判断lambda表达式,为false则解锁互斥量并阻塞在本行;为true则wait()返回,并继续向下执行流程(此时互斥量一直是被锁的状态);如果wait()函数没有第二个参数,则wait()返回并继续向下执行流程;

    condition_variable_any

    一种通用的条件变量,可以与任意的mutex搭配使用,包括自定义的锁类型

    std::async、std::future:创建后台任务并返回值:

    std::async函数模板:用来启动一个异步任务,并返回std::future对象(类模板对象)。

    std::future对象中含有线程入口函数所返回的结果,能通过其成员函数get()来获取结果

    // 自动创建一个“异步线程”(执行“异步任务”)并开始执行对应的线程入口函数,最终返回一个std::future对象
    std::future<int> result = std::async(mythread);
    		
    cout << result.get() << endl;
    
    • 1
    • 2
    • 3
    • 4

    get()函数,一直在等待,除非拿到结果(且只能调用一次)。

    std::native_handle()函数:

    对操作系统的线程库进行封装,都会损失一部分功能,为了弥补c++11线程库的不足,thread类提供了native_handle()成员函数:用于获得与操作系统相关的原生线程句柄。

    注:操作系统的原生线程库,就可以用原生线程句柄操作线程。

    原子类型#include

    c++11提供的模板类atomic,其模板参数可以是bool、int、long、long long、指针类型(不支持浮点数和自定义类型)。

    原子操作介绍:

    由CPU指令提供支持性能比锁和消息传递效率更高,且支持修改、读取、交换、比较并交换等操作。

    成员函数:

    在这里插入图片描述

    /* 构造函数:*/
    atomic() noexcept = default;             // 默认构造函数
    constexpr atomic(T desired) noexcept;    // 转换函数
    atomic(const atomic&) = delete;          // 拷贝构造函数
    
    /* 赋值函数(禁用):*/
    atomic& operator=(const atomic&) = delete;         
    
    /* 常用的函数:*/
    void store(T desired, std::memory order order = std::memory order seq cst) noexcept;
    // desired:存储到原子变量中的值、order:强制的内存顺序
    
    /* 将desired值存入原子变量中:*/
    T load(std::memory order order = std::memory order seq cst) const noexcept;
    // 原子地加载并返回原子变量的当前值。按照order的值影响内存。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    使用细节:

    • atomic模板类的模板参数是指针,表示该指针是原子类型,但不表示它所指向的对象是原子类型。
    • atomic模板类重载了整数操作的各种运算符。
    • 原子整型可用作“计数器”,布尔型可用作开关。
    • CAS指令,是实现“无锁队列”的基础。
  • 相关阅读:
    微信picker弹出之后 , 背景变成灰色是怎么做的
    一次性掌握innodb引擎如何解决幻读和不可重复读
    数字世界的艺术家_1024特别纪念篇
    Git回滚代码到某个commit(图文讲解 仅需2步)
    【2014NOIP普及组】T3. 螺旋矩阵 试题解析
    GUI编程--PyQt5--QMessageBox
    HIVE调优
    超分辨率重建——SESR网络训练并推理测试(详细图文教程)
    Android Span进阶之路——ClickableSpan
    释放潜能!RunnerGo:性能测试的全新视角
  • 原文地址:https://blog.csdn.net/qq_44599368/article/details/133934896