c++_learning-并发与多线程

并发与多线程

  • 并发:
  • 进程:
  • 线程:
    • 基本概念:
    • 线程安全:
      • 问题出现的场景:
      • 涉及的性质:
      • 如何保证线程安全?
  • 并发的实现手段(优先使用多线程并发):
    • 多进程并发中,进程之间的通信IPC:
    • 多线程并发,单进程中创建多个线程来实现并发:
  • 线程的启动、结束和创建多线程的方法:
    • 创建线程的要点:
      • 给子线程入口函数传递参数时,要用值传递(不能用引用或者指针):
      • 传递类对象、智能指针、类成员函数指针作为线程参数:
    • 创建和等待多个线程:
      • 将多个线程对象放入thread数组:
      • 将多个线程对象放入容器:
  • 互斥锁:
    • 多个线程的数据共享:
      • 数据共享问题分析:
      • 解决的方法:引入互斥量mutex,
      • c++11中提供四种互斥锁:
        • mutex互斥锁:
        • timed_mutex带超时机制的互斥锁:
        • recursive_mutex递归互斥锁:
        • recursive_timed_mutex带超时机制的递归互斥锁
      • 互斥量mutex:
        • 互斥量可以理解为一把锁:
        • mutex类:
      • C++中的死锁:
      • `unique_lock`和`lock_guard`都是管理锁的辅助类:
        • 为防止mutex加锁后忘记unlock(),引出std::lock_guard()类模板:
        • 类模板unique_lock取代lock_guard:
          • unique_lock和lock_guard的**异同点:**
          • `unique_lock` 的**第二个参数**:
          • unique_lock的成员函数:
          • unique_lock**所有权的转移**:
      • 线程的移动和交换
    • 单例设计模式共享数据的问题:
      • 懒汉模式:
      • “饿汉模式”线程安全:
    • 条件变量:std::condition_variable、wait()、notify_one():
      • 条件变量是一种线程同步机制:
      • 常使用条件变量的场景:生产-消费者模型(高速缓存队列):
      • c++11的`#include`条件变量提供了两个类:
        • `condition_variable`:
          • 成员函数:
          • 当另一个线程的notify_one()函数将本线程中wait()函数激活后,
        • `condition_variable_any`:
  • std::async、std::future:创建后台任务并返回值:
  • std::native_handle()函数:
  • 原子类型`#include`:
    • 原子操作介绍:
    • 成员函数:
    • 使用细节:

并发:

一个程序(单核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    // 返回硬件线程上下文的数量
    
  • 其他创建线程的方法:

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

    用lambda表达式,完成创建。

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

创建线程的要点:

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

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

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

    // 该入口函数的形参中,类对象用常量引用
    myPrint(const int i, const string &mystring) {...}
    
    // 创建子线程时,就生成临时对象并拷贝给入口函数
    thread mythreadObj(myPrint, i, string(mychar));
    
    • 整个过程发生在主线程中;
    • 会调用该类的(有参)构造函数和拷贝构造函数,和一个析构函数(析构的是(有参)构造函数);
    • 临时对象构造时机的捕获:通过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));
    
  3. 类成员函数指针:

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

创建和等待多个线程:

将多个线程对象放入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();
}

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

// 用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();
}

互斥锁:

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且“线程不会阻塞”
*/

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再进行锁的操作,只需进行解锁
    

    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);

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再进行锁的操作,只需进行解锁
类模板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();
    //操作共享数据
    //........
}
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();
unique_lock所有权的转移
  1. 通过std::move()进行所有权转移

    std::unique_lock<std::mutex> lck(mtx); 
    std::unique_lock<std::mutex> lck_move(std::move(lck));   // std::move()移动构造函数使用,转移所有权
    
  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();
    

线程的移动和交换

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

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

懒汉模式:

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

  • 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的锁。	
#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;  // 调用默认构造函数
  • 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;

“饿汉模式”线程安全:

#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;	
}

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

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

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

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

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

c++_learning-并发与多线程_第1张图片

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

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;
};

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)   // 循环阻塞当前线程,直到通知到达且谓词满足 

在这里插入图片描述

当另一个线程的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;

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

std::native_handle()函数:

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

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

原子类型#include

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

原子操作介绍:

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

成员函数:

c++_learning-并发与多线程_第2张图片

/* 构造函数:*/
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的值影响内存。

使用细节:

  • atomic模板类的模板参数是指针,表示该指针是原子类型,但不表示它所指向的对象是原子类型。
  • atomic模板类重载了整数操作的各种运算符。
  • 原子整型可用作“计数器”,布尔型可用作开关。
  • CAS指令,是实现“无锁队列”的基础。

你可能感兴趣的:(c++,开发语言)