IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> c++11并发与多线程 -> 正文阅读

[C++知识库]c++11并发与多线程

c++11并发与多线程

0.引言

测试用例CMakeLists.txt:

cmake_minimum_required(VERSION 3.0)

project(thread_learn)

add_compile_options(-g)
SET(CMAKE_CXX_COMPILER "g++")
set( CMAKE_BUILD_TYPE Debug )
# set( CMAKE_BUILD_TYPE Release )
# 设置编译选项: c++11标准、O3优化、多线程
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -march=native -O3 -pthread" )

find_package (Threads REQUIRED)

include_directories(
    ${PROJECT_SOURCE_DIR}
)

add_executable(thread_learn thread_learn.cpp )

一.基本概念和实现

  • 0.目录
    请添加图片描述

请添加图片描述

1.1.并发、进程、线程的基本概念

并发,线程,进程要求必须掌握

1.1.1 并发

两个或者更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务;

  • 单核CPU:某一个时刻只能执行一个任务,由操作系统调度,每秒钟进行多次所谓的“任务切换”。并发的假象(不是真正的并发),切换(上下文切换)时要保存变量的状态、执行进度等,存在时间开销;
  • 多核CPU:用于服务器和高性能计算领域。台式机:在一块芯片上有多核(一个CPU内有多个运算核心,对于操作系统来说,每个核心都是作为单独的CPU对待的):双核,4核,8核,10核(自己的笔记本是4核8线程的)。能够实现真正的并行执行多个任务(硬件并发)

使用并发的原因:主要就是同时可以干多个事,提高性能

1.1.2 可执行程序

磁盘上的一个文件,windows下,扩展名为.exe;linux下,ls -la,rwx(可读可写可执行)

1.1.3 进程

运行一个可执行程序,在windows下,可双击;在linux下,./文件名
进程,一个可执行程序运行起来了,就叫创建了一个进程。进程就是运行起来的可执行程序。

1.1.4 线程


  • a)每个进程(执行起来的可执行程序),都有唯一的一个主线程
    b)当执行可执行程序时,产生一个进程后,这个主线程就随着这个进程默默启动起来了
    c)ctrl+F5运行这个程序的时候,实际上是进程的主线程来执行(调用)这个main函数中的代码
    线程:用来执行代码的。线程东西,可以理解为一条代码的执行通路。
    请添加图片描述


  • a)除了主线程之外,可以通过写代码来创建其他线程,其他线程走的是别的道路,甚至去不同的地方
    b)每创建一个新线程,就可以在同一时刻,多干一个不同的事(多走一条不同的代码执行路径)


  • a)多线程(并发)
    b)线程并不是越多越好,每个线程,都需要一个独立的堆栈空间(大约1M),线程之间的切换要保存很多中间状态,切换也会耗费本该属于程序运行的时间
    c)必须使用多线程的案例

请添加图片描述

请添加图片描述

1.1.5 学习心得

  • 线程是用来执行代码的;
  • 一个线程就是一个执行通路,一个新线程代表一条新的通路;
  • 一个进程自动包含一个主线程,主线程随着进程默默的启动并运行,我们可以通过编码来创建多个其他线程(非主线程)。但是创建的线程数量不建议超过200个;
  • 因为主线程是自动启动的,所以一个进程中最少也是有一个线程的(主线程),进程和线程感觉是爹和儿子的关系;

1.2.并发的实现方法

实现并发的手段:

  • a)通过多个进程实现并发
  • b)在单独的进程中,写代码创建除了主线程之外的其他线程来实现并发

1.2.1 多进程并发

  • 比如账号服务器一个进程,游戏服务器一个进程。
  • 服务器进程之间存在通信(同一个电脑上:管道,文件,消息队列,共享内存);(不同电脑上:socket通信技术)

1.2.2 多线程并发

  • 线程:感觉像是轻量级的进程。每个进程有自己独立的运行路径,但一个进程中的所有线程共享地址空间(共享内存),全局变量、指针、引用都可以在线程之间传递,所以多线程开销远远小于多进程。共享内存带来的新问题,数据一致性问题:线程A、线程B,不能同时写。
  • 多进程并发和多线程并发可以混合使用,但建议优先考虑多线程技术
  • 本课程中只讲多线程并发技术

1.2.3.总结

  • 和进程相比,线程具有如下优点:
    • 1.线程启动速度更快,更轻量级;
    • 2.系统资源开销更少,执行速度更快,比如共享内存这种通信方式比任何其他的通信方式都快
  • 缺点:
    • 1.使用有一定难度,要小心处理数据一致性问题

1.3.C++11新标准线程库

以往

  • windows:CreateThread(), _beginthread(),_beginthreadexe()创建线程;
  • linux:pthread_create()创建线程;
  • 概念:临界区,互斥锁
  • 不能跨平台
  • 多线程库可跨平台:POSIX thread(pthread):跨平台,但要做一番配置,也不方便

C++11

  • 从C++11新标准,C++语言本身增加对多线程的支持,意味着可移植性(跨平台),这大大减少开发人员的工作量

二.线程启动、结束,创建线程多法、join,detach

  • 目录

请添加图片描述

2.1.范例演示线程运行的开始

  • 程序运行起来,生成一个进程,该进程所属的主线程开始自动运行;当主线程从main()函数返回,则整个进程执行完毕
  • 主线程从main()开始执行,那么我们自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,线程也结束运行
  • 整个进程是否执行完毕的标志是:主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了,此时如果其他子线程还没有执行完,也会被强行终止【此条有例外,以后会解释】

创建一个线程:

  • 1.包含头文件thread
  • 2.写初始函数
  • 3.在main中创建thread

必须要明白:有两个线程在跑,相当于整个程序中有两条线在同时走,即使一条被阻塞,另一条也能运行

#include <iostream>
#include <thread>
using namespace std;

void myPrint() {
  cout << "我的线程开始运行" << endl;
  //-------------
  //-------------
  cout << "我的线程运行完毕" << endl;
  return;
}

int main() {
  //(1)创建线程,线程执行起点(入口)是myPrint;(2)执行线程
  thread myThread(myPrint);
  cout << "Hello World!" << endl;
  return 0;
}

多次运行结果:

Hello World!我的线程开始运行
terminate called without an active exception
Aborted (core dumped)
--------
Hello World!
我的线程开始运行
我的线程运行完毕
terminate called without an active exception
Aborted (core dumped)
--------
Hello World!
我的线程开始运行terminate called without an active exception

我的线程运行完毕
Aborted (core dumped)
-------- 
我的线程开始运行
我的线程运行完毕
Hello World!
terminate called without an active exception
Aborted (core dumped)
--------
Hello World!
terminate called without an active exception
我的线程开始运行
我的线程运行完毕
Aborted (core dumped)

两个线程同时运行,谁快谁慢不一定。terminate called without an active exception 报错是因为,main函数主线程运行完了,myprint线程还没运行完导致,即主线程结束,子线程未结束;。

#include <iostream>
#include <thread>
using namespace std;

void myPrint() {
  cout << "我的线程开始运行" << endl;
  //-------------
  //-------------
  cout << "我的线程运行完毕" << endl;
  return;
}


int main() {
  // (1)创建线程,线程执行起点(入口)是myPrint;(2)执行线程
  thread myThread(myPrint);
  cout << "Hello World-1!" << endl;
  // join意为汇合,子线程和主线程汇合,join会阻塞主线程并等待myPrint执行完,
  // 当myPrint执行完毕,join()就执行完毕,主线程才继续往下执行
  myThread.join();

  cout << "Hello World-2!" << endl;
  return 0;
}

多次运行结果均为:

Hello World-1!
我的线程开始运行
我的线程运行完毕
Hello World-2!

join意为汇合,子线程和主线程汇合,join会阻塞主线程并等待myPrint执行完

请添加图片描述

#include <iostream>
#include <thread>
using namespace std;

void myPrint() {
  cout << "我的线程开始运行" << endl;
  //-------------
  //-------------
  cout << "我的线程运行完毕" << endl;
  return;
}

int main() {
  //(1)创建了线程,线程执行起点(入口)是myPrint;(2)执行线程
  thread myThread(myPrint);
  cout << "Hello World-1!" << endl;

  // join意为汇合,子线程和主线程汇合,join会阻塞主线程并等待myPrint执行完,
  //当myPrint执行完毕,join()就执行完毕,主线程继续往下执行
  // myThread.join();

  //(3)传统多线程程序中,主线程要等待子线程执行完毕,然后自己才能向下执行
  // detach:分离,主线程不再与子线程汇合,不再等待子线程
  // detach后,子线程和主线程失去关联,驻留在后台,由C++运行时库接管
  myThread.detach();

  cout << "Hello World-2!" << endl;
  return 0;
}

join与detach不能同时使用,顾名思义,一个分离一个汇合,肯定无法同时使用。否则报错:

terminate called after throwing an instance of 'std::system_error'
  what():  Invalid argument
Aborted (core dumped)

多次运行结果:

Hello World-1!
Hello World-2!
我的线程开始运行
我的线程运行完毕
--------
Hello World-1!我的线程开始运行

我的线程运行完毕
Hello World-2!
--------
Hello World-1!我的线程开始运行
Hello World-2!

我的线程运行完毕
--------
Hello World-1!我的线程开始运行
我的线程运行完毕

Hello World-2!
--------
Hello World-1!
我的线程开始运行Hello World-2!
我的线程运行完毕

--------
Hello World-1!
Hello World-2!我的线程开始运行
--------
Hello World-1!
Hello World-2!
-------- 
#include <iostream>
#include <thread>
using namespace std;

void myPrint() {
  cout << "我的线程开始运行" << endl;
  //-------------
  //-------------
  cout << "我的线程运行完毕" << endl;
  return;
}

int main() {
  //(1)创建了线程,线程执行起点(入口)是myPrint;(2)执行线程
  thread myThread(myPrint);
  cout << "Hello World-1!" << endl;

  // join意为汇合,子线程和主线程汇合,join会阻塞主线程并等待myPrint执行完,
  // 当myPrint执行完毕,join()就执行完毕,主线程继续往下执行
  // myThread.join();
  //(3)传统多线程程序中,主线程要等待子线程执行完毕,然后自己才能向下执行
  // detach:分离,主线程不再与子线程汇合,不再等待子线程
  // detach后,子线程和主线程失去关联,驻留在后台,由C++运行时库接管
  // myThread.detach();

  //(4)joinable()判断是否可以成功使用join()或者detach()
  //如果返回true,证明可以调用join()或者detach()
  //如果返回false,证明调用过join()或者detach(),join()和detach()都不能再调用了
  if (myThread.joinable()) {
    cout << "可以调用可以调用join()或者detach()" << endl;
  } else {
    cout << "不能调用可以调用join()或者detach()" << endl;
  }

  cout << "Hello World-2!" << endl;
  return 0;
}

多次调用结果:

--------
Hello World-1!
可以调用可以调用join()或者detach()
Hello World-2!
terminate called without an active exception
Aborted (core dumped)
--------
Hello World-1!我的线程开始运行
可以调用可以调用join()或者detach()
Hello World-2!

terminate called without an active exception
我的线程运行完毕
Aborted (core dumped)
--------
Hello World-1!我的线程开始运行

我的线程运行完毕
可以调用可以调用join()或者detach()
Hello World-2!
terminate called without an active exception
Aborted (core dumped)
--------
Hello World-1!我的线程开始运行
可以调用可以调用join()或者detach()
我的线程运行完毕

Hello World-2!
terminate called without an active exception
Aborted (core dumped)
-------

terminate called without an active exception 报错是因为,main函数主线程运行完了,myprint线程还没运行完导致,即主线程结束,子线程未结束;。

#include <iostream>
#include <thread>
using namespace std;

void myPrint() {
  cout << "我的线程开始运行" << endl;
  //-------------
  //-------------
  cout << "我的线程运行完毕" << endl;
  return;
}

int main() {
  //(1)创建了线程,线程执行起点(入口)是myPrint;(2)执行线程
  thread myThread(myPrint);
  cout << "Hello World-1!" << endl;

  // join意为汇合,子线程和主线程汇合,join会阻塞主线程并等待myPrint执行完,
  //当myPrint执行完毕,join()就执行完毕,主线程继续往下执行
  myThread.join();
  //(3)传统多线程程序中,主线程要等待子线程执行完毕,然后自己才能向下执行
  // detach:分离,主线程不再与子线程汇合,不再等待子线程
  // detach后,子线程和主线程失去关联,驻留在后台,由C++运行时库接管
  // myThread.detach();

  //(4)joinable()判断是否可以成功使用join()或者detach()
  //如果返回true,证明可以调用join()或者detach()
  //如果返回false,证明调用过join()或者detach(),join()和detach()都不能再调用了
  if (myThread.joinable()) {
    cout << "可以调用可以调用join()或者detach()" << endl;
  } else {
    cout << "不能调用可以调用join()或者detach()" << endl;
  }

  cout << "Hello World-2!" << endl;
  return 0;
}

多次调用结果均为:

Hello World-1!
我的线程开始运行
我的线程运行完毕
不能调用可以调用join()或者detach()
Hello World-2!

重要补充线程类参数是一个可调用对象。

  • 一组可执行的语句称为可调用对象,c++中的可调用对象可以是函数、函数指针、lambda表达式、bind创建的对象或者重载了函数调用运算符的类对象

2.2.其他创建线程的方法

  • ①创建一个类,并编写圆括号重载函数,初始化一个该类的对象,把该对象作为线程入口地址
#include <iostream>
#include <thread>
using namespace std;
class Ta {
 public:
  void operator()()  //不能带参数
  {
    cout << "我的线程开始运行" << endl;
    //-------------
    //-------------
    cout << "我的线程运行完毕" << endl;
  }
};

int main() {
  cout << "Hello World-1!" << endl;
  Ta ta;
  //(1)创建线程,线程执行起点(入口)是myPrint;(2)执行线程
  thread myThread(ta);
  myThread.join();
  cout << "Hello World-2!" << endl;
  return 0;
}

运行结果:

Hello World-1!
我的线程开始运行
我的线程运行完毕
Hello World-2!
  • ②lambda表达式创建线程
#include <iostream>
#include <thread>
using namespace std;
auto lambdaThread = [] {
  cout << "我的线程开始执行了" << endl;
  //-------------
  //-------------
  cout << "我的线程开始执行了" << endl;
};

int main() {
  cout << "Hello World-1!" << endl;
  //(1)创建线程,线程执行起点(入口)是myPrint;(2)执行线程
  thread myThread(lambdaThread);
  myThread.join();
  cout << "Hello World-2!" << endl;
  return 0;
}

运行结果:

Hello World-1!
我的线程开始运行
我的线程运行完毕
Hello World-2!
  • ③把某个类中的某个函数作为线程的入口地址
#include <iostream>
#include <thread>
using namespace std;
class Data_ {
 public:
  void GetMsg() { std::cout << "fun GetMsg is run!\n"; }
  void SaveMsh() { std::cout << "fun SaveMsh is run!!\n"; }
};

int main() {
  cout << "Hello World-1!" << endl;
  Data_ s;
  // 第一个&:取址,第二个&:引用
  // thread oneobj(&Data_::SaveMsh,s)传值也可以
  // 在其他的构造函数中&obj不会代表引用,会被当成取地址
  // 调用方式:对象成员函数地址,类实例,[成员函数参数]
  // 第二个参数可以传递对象s,也可以传递引用std::ref(s)或&s
  // 传递s,会调用拷贝构造函数在子线程中生成一个新的对象
  // 传递&,子线程中还是用的原来的对象,所以就不能detach,因为主线程运行完毕会把该对象释放掉
  thread oneobj(&Data_::SaveMsh, &s);
  thread twoobj(&Data_::GetMsg, &s);
  oneobj.join();
  twoobj.join();
  cout << "Hello World-2!" << endl;
  return 0;
}

运行结果:

Hello World-1!
fun SaveMsh is run!!
fun GetMsg is run!
Hello World-2!

三.线程传参详解,detach()大坑,成员函数做线程函数

  • 目录

请添加图片描述

3.1.传递临时对象作为线程参数

  • 要避免的陷阱1:
#include <iostream>
#include <thread>
using namespace std;

void myPrint(const int& i, char* pmybuf) {
  // 如果线程从主线程detach了
  // i不是mvar真正的引用,实际上值传递,即使主线程运行完毕了,子线程用i仍然是安全的,但仍不推荐传递引用
  // 推荐改为const int i
  cout << i << endl;
  // pmybuf还是指向原来的字符串,所以这么写是不安全的
  cout << pmybuf << endl;
}

int main() {
  int mvar = 10;
  char mybuf[] = "this is a test";
  //第一个参数是函数名,后两个参数是函数的参数
  thread myThread(myPrint, mvar, mybuf);
  mybuf[0] = 'T';
  myThread.join();
  mybuf[0] = 't';
  // myThread.detach();
  cout << "Hello World!" << endl;
}

运行结果:

10
This is a test
Hello World!

为什么是大写,参考:

请添加图片描述

  • 要避免的陷阱2:
#include <iostream>
#include <string>
#include <thread>
using namespace std;

void myPrint(const int i, const string& pmybuf) {
  cout << i << endl;
  cout << pmybuf << endl;
}

int main() {
  int mvar = 10;
  char mybuf[] = "this is a test";
  //如果detach了,这样仍然是不安全的
  //因为存在主线程运行完了,mybuf被回收了,系统采用mybuf隐式类型转换成string
  //推荐先创建一个临时对象thread myThread(myPrint, mvar,
  //string(mybuf));就绝对安全了。。。。
  thread myThread(myPrint, mvar, mybuf);
  // myThread.join();
  myThread.detach();

  cout << "Hello World!" << endl;
}

多次运行结果,结果不可控:

Hello World!
10
this is a test
--------
Hello World!
10

--------
Hello World!
10
this is a test
--------
Hello World!
10
this is a test
#include <iostream>
#include <string>
#include <thread>
using namespace std;

void myPrint(const int i, const string& pmybuf) {
  cout << i << endl;
  cout << pmybuf << endl;
}

int main() {
  int mvar = 10;
  char mybuf[] = "this is a test";
  //如果detach了,这样仍然是不安全的
  //因为存在主线程运行完了,mybuf被回收了,系统采用mybuf隐式类型转换成string
  // 推荐先创建一个临时对象
  thread myThread(myPrint, mvar,string(mybuf));//就绝对安全了。。。。
  // thread myThread(myPrint, mvar, mybuf);
  // myThread.join();
  myThread.detach();

  cout << "Hello World!" << endl;
}

多次运行结果:

Hello World!
--------
Hello World!10
this is a test

--------
Hello World!
10
this is a test
--------
Hello World!
10
this is a test
  • 总结

  • 1.如果传递int这种简单类型,推荐使用值传递,不要用引用

  • 2.如果传递类对象,避免使用隐式类型转换,全部都是创建线程这一行就创建出临时对象,然后在函数参数里,用引用来接,否则还会创建出一个对象

  • 3.终极结论:建议不使用detach

3.2.临时对象作为线程参数继续讲

  • 线程id概念

  • id是个数字,每个线程(不管是主线程还是子线程)实际上都对应着一个数字,而且每个线程对应的这个数字都不一样

  • 线程id可以用C++标准库里的函数来获取。std::this_thread::get_id()来获取

#include <iostream>
#include <string>
#include <thread>
using namespace std;

void myPrint(const int i, const string& pmybuf) {
  cout << i << endl;
  cout << pmybuf << endl;
  cout << std::this_thread::get_id() << std::endl;
}

int main() {
  int mvar = 10;
  char mybuf[] = "this is a test";
  thread myThread(myPrint, mvar, string(mybuf));
  cout << std::this_thread::get_id() << std::endl;
  myThread.join();
  cout << "Hello World!" << endl;
}

多次运行结果:

139715575359296
10
this is a test
139715533989632
Hello World!
--------
139966551197504
10
this is a test
139966509827840
Hello World!
--------
140299849795392
10
this is a test
140299808425728
Hello World!

3.3.传递类对象、智能指针作为线程参数

  • 传递类对象
#include <iostream>
#include <thread>
using namespace std;

class A {
 public:
  mutable int m_i;  //mutable: m_i即使实在const中也可以被修改
  A(int i) : m_i(i) {}
};

void myPrint(const A& pmybuf) {
  pmybuf.m_i = 199;
  cout << "子线程myPrint的参数地址是" << &pmybuf << endl
       << "thread = " << std::this_thread::get_id() << endl;
}

int main() {
  A myObj(10);
  // myPrint(const A& pmybuf)中引用不能去掉,如果去掉会多创建一个对象
  // const也不能去掉,去掉会出错
  // 即使是传递的const引用,但在子线程中还是会调用拷贝构造函数构造一个新的对象,
  // 所以在子线程中修改m_i的值不会影响到主线程
  // 如果希望子线程中修改m_i的值影响到主线程,可以用thread myThread(myPrint, std::ref(myObj));
  // 这样const就是真的引用了,myPrint定义中的const就可以去掉了,类A定义中的mutable也可以去掉了
  thread myThread(myPrint, myObj);
  myThread.join();
  // myThread.detach();

  cout << "Hello World!" << endl;
}

运行结果:

子线程myPrint的参数地址是0x55aac9ce2e78
thread = 140061621405440
Hello World!
  • 传智能指针
#include <iostream>
#include <memory>
#include <thread>
using namespace std;

void myPrint(unique_ptr<int> ptn) {
  cout << "thread = " << std::this_thread::get_id() << endl;
}

int main() {
  unique_ptr<int> up(new int(10));
  //独占式指针只能通过std::move()才可以传递给另一个指针
  //传递后up就指向空,新的ptn指向原来的内存
  //所以这时就不能用detach了,因为如果主线程先执行完,ptn指向的对象就被释放了
  thread myThread(myPrint, std::move(up));
  myThread.join();
  // myThread.detach();
  return 0;
}

运行结果:

thread = 139634421532416

四.创建多个线程、数据共享问题分析、案例代码

请添加图片描述

4.1.创建和等待多个线程

#include <iostream>
#include <memory>
#include <thread>
#include <vector>
using namespace std;

void TextThread() {
  cout << "我是线程" << this_thread::get_id() << endl;
  /*  …  */
  cout << "线程" << this_thread::get_id() << "执行结束" << endl;
}

int main() {
  std::vector<thread> threadagg;
  for (int i = 0; i < 5; ++i) {
    threadagg.push_back(thread(TextThread));
  }
  cout<<"hello!\n";
  for (int i = 0; i < 5; ++i) {
    threadagg[i].join();
  }
  return 0;
}

运行结果:

我是线程我是线程140472505550592140472497157888
线程140472505550592执行结束
我是线程我是线程140472408340224
线程140472408340224
hello!
140472488765184
线程140472488765184执行结束
线程140472497157888执行结束
执行结束
我是线程140472399947520
线程140472399947520执行结束
--------
我是线程我是线程140031451133696140031467919104
我是线程140031368886016
线程线程140031451133696执行结束
140031368886016执行结束
hello!

线程140031467919104执行结束
我是线程140031459526400
线程140031459526400执行结束
我是线程140031360493312
线程140031360493312执行结束
  • 把thread对象放入到容器中管理,看起来像个thread对象数组,对一次创建大量的线程并对大量线程进行管理有好处
  • 但实际上多个线程执行顺序是乱的,跟操作系统内部对线程的运行调度机制有关

4.2.数据共享问题分析

  • 只读的数据:是安全稳定的
  • 有读有写:若不加处理,就会出错,最简单的防止崩溃方法:读的时候不能写,写的时候不能读。写的动作分10小步,由于任务切换,导致各种诡异的事情发生(最可能的还是崩溃)

五.互斥锁概念、用法、死锁演示及解决详解

请添加图片描述

  • mutex类4种
    • std::mutex,最基本的 Mutex 类。
    • std::recursive_mutex,递归 Mutex 类。
    • std::time_mutex,定时 Mutex 类。
    • std::recursive_timed_mutex,定时递归 Mutex 类。
  • Lock 类(两种)
    • std::lock_guard,与 Mutex RAII 相关,方便线程对互斥锁上锁。
    • std::unique_lock,与 Mutex RAII 相关,方便线程对互斥锁上锁,但提供了更好的上锁和解锁控制。
  • 其他类型
    • std::once_flag
    • std::adopt_lock_t
    • std::defer_lock_t
    • std::try_to_lock_t
  • 函数
    • std::try_lock,尝试同时对多个互斥锁上锁。
    • std::lock,可以同时对多个互斥锁上锁。
    • std::call_once,如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。

5.1.互斥锁(mutex)的基本概念

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

std::mutex 的成员函数

  • 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
  • lock(),调用线程将锁住该互斥锁。线程调用该函数会发生下面 3 种情况:
    • (1). 如果该互斥锁当前没有被锁住,则调用线程将该互斥锁锁住,直到调用 - unlock之前,该线程一直拥有该锁。
    • (2). 如果当前互斥锁被其他线程锁住,则当前的调用线程被阻塞住。
    • (3). 如果当前互斥锁被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock(), 解锁,释放对互斥锁的所有权。
  • try_lock(),尝试锁住互斥锁,如果互斥锁被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,
    • (1). 如果当前互斥锁没有被其他线程占有,则该线程锁住互斥锁,直到该线程调用 unlock 释放互斥锁。
    • (2). 如果当前互斥锁被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
    • (3). 如果当前互斥锁被当前调用线程锁住,则会产生死锁(deadlock)。

5.2.互斥锁的用法

包含#include <mutex>头文件

5.2.1 lock(),unlock()

  • 步骤:1.lock(),2.操作共享数据,3.unlock()
  • lock()和unlock()要成对使用

请添加图片描述

// 不加锁

// mutex example
#include <iostream>  // std::cout
#include <mutex>     // std::mutex
#include <thread>    // std::thread

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

void print_block(int n, char c) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  // mtx.lock();
  for (int i = 0; i < n; ++i) {
    std::cout << c;
  }
  std::cout << '\n';
  // mtx.unlock();
}

int main() {
  std::thread th1(print_block, 50, '*');
  std::thread th2(print_block, 50, '$');
  th1.join();
  th2.join();
  return 0;
}

运行结果不可控:

**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
--------
******************************$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$*$***$****$*$$$$$*$$$******$*$*$$$*$
*
--------
*******************************$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
*******************
--------
**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
--------
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
**************************************************
--------
*******************************$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
*******************

加锁:


// mutex example
#include <iostream>  // std::cout
#include <mutex>     // std::mutex
#include <thread>    // std::thread

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

void print_block(int n, char c) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx.lock();
  for (int i = 0; i < n; ++i) {
    std::cout << c;
  }
  std::cout << '\n';
  mtx.unlock();
}

int main() {
  std::thread th1(print_block, 50, '*');
  std::thread th2(print_block, 50, '$');
  th1.join();
  th2.join();
  return 0;
}

运行结果:

**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
--------
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
**************************************************
--------
**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
--------
**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
--------
**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

再看一个例子,两个线程竞争全局变量g_num对其进行写操作,然后打印输出:

#include <iostream>
#include <mutex>   // std::mutex
#include <thread>  // std::thread

int g_num = 0;     // 为 g_num_mutex 所保护
std::mutex g_num_mutex;

void slow_increment(int id) {
  for (int i = 0; i < 3; ++i) {
    ++g_num;
    std::cout << "th" << id << " => " << g_num << '\n';
  }
}

int main() {
  std::thread t1(slow_increment, 0);
  std::thread t2(slow_increment, 1);
  t1.join();
  t2.join();
}

运行结果:

thth0 => 2
th0 => 3
th0 => 4
1 => 4
th1 => 5
th1 => 6
--------
thth1 => 02
th1 => 3
th1 => 4
 => 4
th0 => 5
th0 => 6
--------
thth0 => 2
th0 => 3
th0 => 4
1 => 4
th1 => 5
th1 => 6
  • 加锁
#include <iostream>
#include <mutex>   // std::mutex
#include <thread>  // std::thread

int g_num = 0;     // 为 g_num_mutex 所保护
std::mutex g_num_mutex;

void slow_increment(int id) {
  for (int i = 0; i < 3; ++i) {
    g_num_mutex.lock();
    ++g_num;
    std::cout << "th" << id << " => " << g_num << '\n';
    g_num_mutex.unlock();
  }
}

int main() {
  std::thread t1(slow_increment, 0);
  std::thread t2(slow_increment, 1);
  t1.join();
  t2.join();
}

运行结果:

th0 => 1
th0 => 2
th0 => 3
th1 => 4
th1 => 5
th1 => 6
  • 加锁,降低运行速度
#include <chrono>  // std::chrono
#include <iostream>
#include <mutex>   // std::mutex
#include <thread>  // std::thread

int g_num = 0;     // 为 g_num_mutex 所保护
std::mutex g_num_mutex;

void slow_increment(int id) {
  for (int i = 0; i < 3; ++i) {
    g_num_mutex.lock();
    ++g_num;
    std::cout << "th" << id << " => " << g_num << '\n';
    g_num_mutex.unlock();
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
}

int main() {
  std::thread t1(slow_increment, 0);
  std::thread t2(slow_increment, 1);
  t1.join();
  t2.join();
}

运行结果

th0 => 1
th1 => 2
th0 => 3
th1 => 4
th1 => 5
th0 => 6
--------
th0 => 1
th1 => 2
th0 => 3
th1 => 4
th0 => 5
th1 => 6
--------
th0 => 1
th1 => 2
th0 => 3
th1 => 4
th0 => 5
th1 => 6

5.2.2 lock_guard类模板

  • lock_guard sbguard(myMutex);取代lock()和unlock()
  • lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()

lock_guard类模板可直接取代lock( )、unlack( ),并且使用lock_guard类模板后不能再对这个保护区域使用lock( )、unlack( ).?lock_guard类模板超出作用域时会自动析构释放锁。

#include <chrono>  // std::chrono
#include <iostream>
#include <mutex>   // std::mutex
#include <thread>  // std::thread

int g_num = 0;     // 为 g_num_mutex 所保护
std::mutex g_num_mutex;

void slow_increment(int id) {
  for (int i = 0; i < 3; ++i) {
    std::lock_guard<std::mutex> m_guard(g_num_mutex);
    ++g_num;
    std::cout << "th" << id << " => " << g_num << '\n';
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
}

int main() {
  std::thread t1(slow_increment, 0);
  std::thread t2(slow_increment, 1);
  t1.join();
  t2.join();
}

运行结果:

th0 => 1
th0 => 2
th0 => 3
th1 => 4
th1 => 5
th1 => 6
--------
th1 => 1
th1 => 2
th1 => 3
th0 => 4
th0 => 5
th0 => 6
--------
th0 => 1
th0 => 2
th0 => 3
th1 => 4
th1 => 5
th1 => 6

5.2.3 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()。

  • 原博文和这篇好像

5.3.死锁

5.3.1 死锁演示

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

  • a.线程A执行,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
  • b.线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
  • c.此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。
    请添加图片描述

5.3.2 死锁的一般解决方案:

1、以确定的顺序获得锁

按照上面的例子,两个线程获得锁的时序图如下:

请添加图片描述

如果此时把获得锁的时序改成:

请添加图片描述

那么死锁就永远不会发生。

2、超时放弃

该方法可以按照固定时长等待锁,线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。 还是按照之前的例子,时序图如下:

请添加图片描述


死锁例子:

#include <chrono>  // std::chrono
#include <iostream>
#include <mutex>  // std::mutex
#include <queue>
#include <thread>  // std::thread

using namespace std;

class JackTang {
 public:
  int number;
  JackTang(int a) : number(a) {}

  JackTang(const JackTang &obj) { number = obj.number; }
  ~JackTang() {}

  void consumer() {
    for (int i = 0; i < 10000; i++) {
      m_utex2.lock();
      //处理某些任务...
      m_utex1.lock();

      if (!m_que.empty()) {
        cout << "取出元素:" << m_que.front() << endl;
        m_que.pop();
      }

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  void productor() {
    for (int i = 1; i < 10000; i++) {
      m_utex1.lock();
      //处理某些任务...
      m_utex2.lock();

      m_que.push(i);
      cout << "插入元素:" << i << endl;

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  std::mutex m_utex1;
  std::mutex m_utex2;
  std::queue<int> m_que;
};

int main() {
  JackTang j(1);
  // 类中成员函数作为线程入口, 参考 2.2.其他创建线程的方法
  // 调用方式:对象成员函数地址,类实例,[成员函数参数]
  std::thread t1(&JackTang::consumer, &j);
  std::thread t2(&JackTang::productor, &j);

  t1.detach();
  t2.detach();
  return 0;
}

运行结果:

取出元素:255691592
free(): invalid pointer
thread_learn: ../nptl/pthread_mutex_lock.c:117: __pthread_mutex_lock: Assertion `mutex->__data.__owner == 0' failed.
Aborted (core dumped)

请添加图片描述

更改:

#include <chrono>  // std::chrono
#include <iostream>
#include <mutex>  // std::mutex
#include <queue>
#include <thread>  // std::thread

using namespace std;

class JackTang {
 public:
  int number;
  JackTang(int a) : number(a) {}

  JackTang(const JackTang &obj) { number = obj.number; }
  ~JackTang() {}

  void consumer() {
    for (int i = 0; i < 10; i++) {
      m_utex1.lock();
      //处理某些任务...
      m_utex2.lock();

      if (m_que.empty()) {
        cout << "m_que队列为空" << endl;
      } else {
        cout << "取出元素:" << m_que.front() << endl;
        m_que.pop();
      }

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  void productor() {
    for (int i = 0; i < 10; i++) {
      m_utex1.lock();
      //处理某些任务...
      m_utex2.lock();

      m_que.push(i);
      cout << "插入元素:" << i << endl;

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  std::mutex m_utex1;
  std::mutex m_utex2;
  std::queue<int> m_que;
};

int main() {
  JackTang j(1);
  // 类中成员函数作为线程入口, 参考 2.2.其他创建线程的方法
  // 调用方式:对象成员函数地址,类实例,[成员函数参数]
  std::thread t2(&JackTang::productor, &j);
  std::thread t1(&JackTang::consumer, &j);

  t1.detach();
  t2.detach();
  return 0;
}

运行结果:

--------
Segmentation fault (core dumped)
--------
Segmentation fault (core dumped)
--------
取出元素:322800456
取出元素:-402639669
取出元素:-125994
取出元素:1866304328
取出元素:-402639669
取出元素:-126006
取出元素:859671368
取出元素:-402639672
取出元素:-121714
Segmentation fault (core dumped)

分析原因:detach分离后,main函数运行太快。再改,降低main函数运行速度:

#include <chrono>  // std::chrono
#include <iostream>
#include <mutex>  // std::mutex
#include <queue>
#include <thread>  // std::thread

using namespace std;

class JackTang {
 public:
  int number;
  JackTang(int a) : number(a) {}

  JackTang(const JackTang &obj) { number = obj.number; }
  ~JackTang() {}

  void consumer() {
    for (int i = 0; i < 10; i++) {
      m_utex1.lock();
      //处理某些任务...
      m_utex2.lock();

      if (m_que.empty()) {
        cout << "m_que队列为空" << endl;
      } else {
        cout << "取出元素:" << m_que.front() << endl;
        m_que.pop();
      }

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  void productor() {
    for (int i = 0; i < 10; i++) {
      m_utex1.lock();
      //处理某些任务...
      m_utex2.lock();

      m_que.push(i);
      cout << "插入元素:" << i << endl;

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  std::mutex m_utex1;
  std::mutex m_utex2;
  std::queue<int> m_que;
};

int main() {
  JackTang j(1);
  // 类中成员函数作为线程入口, 参考 2.2.其他创建线程的方法
  // 调用方式:对象成员函数地址,类实例,[成员函数参数]
  std::thread t2(&JackTang::productor, &j);
  std::thread t1(&JackTang::consumer, &j);

  t1.detach();
  t2.detach();
  std::this_thread::sleep_for(std::chrono::milliseconds(1));
  return 0;
}

运行结果:

插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
取出元素:0
取出元素:1
取出元素:2
取出元素:3
取出元素:4
取出元素:5
取出元素:6
取出元素:7
取出元素:8
取出元素:9
--------
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
取出元素:0
取出元素:1
取出元素:2
取出元素:3
取出元素:4
取出元素:5
取出元素:6
取出元素:7
取出元素:8
取出元素:9
--------
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9

如果把consumer放前面:

#include <chrono>  // std::chrono
#include <iostream>
#include <mutex>  // std::mutex
#include <queue>
#include <thread>  // std::thread

using namespace std;

class JackTang {
 public:
  int number;
  JackTang(int a) : number(a) {}

  JackTang(const JackTang &obj) { number = obj.number; }
  ~JackTang() {}

  void consumer() {
    for (int i = 0; i < 10; i++) {
      m_utex1.lock();
      //处理某些任务...
      m_utex2.lock();

      if (m_que.empty()) {
        cout << "m_que队列为空" << endl;
      } else {
        cout << "取出元素:" << m_que.front() << endl;
        m_que.pop();
      }

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  void productor() {
    for (int i = 0; i < 10; i++) {
      m_utex1.lock();
      //处理某些任务...
      m_utex2.lock();

      m_que.push(i);
      cout << "插入元素:" << i << endl;

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  std::mutex m_utex1;
  std::mutex m_utex2;
  std::queue<int> m_que;
};

int main() {
  JackTang j(1);
  // 类中成员函数作为线程入口, 参考 2.2.其他创建线程的方法
  // 调用方式:对象成员函数地址,类实例,[成员函数参数]
  std::thread t1(&JackTang::consumer, &j);
  std::thread t2(&JackTang::productor, &j);

  t1.detach();
  t2.detach();
  std::this_thread::sleep_for(std::chrono::milliseconds(1));
  return 0;
}

运行结果:

m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
--------
插入元素:0
插入元素:1
插入元素:2
取出元素:0
取出元素:1
插入元素:3
插入元素:4
插入元素:5
取出元素:2
取出元素:3
取出元素:4
取出元素:5
m_que队列为空
m_que队列为空
m_que队列为空
插入元素:6
插入元素:7
插入元素:8
插入元素:9
取出元素:6
--------
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
--------
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
取出元素:0
取出元素:1
取出元素:2
取出元素:3
取出元素:4

5.3.3 std::lock()函数模板

  • std::lock(mutex1,mutex2……); 一次锁定多个互斥锁(一般这种情况很少),用于处理多个互斥锁。
  • 如果互斥锁中一个没锁住,它就等着,等所有互斥锁都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥锁都锁住,要么都没锁住,防止死锁)
#include <chrono>  // std::chrono
#include <iostream>
#include <mutex>  // std::mutex
#include <queue>
#include <thread>  // std::thread

using namespace std;

class JackTang {
 public:
  int number;
  JackTang(int a) : number(a) {}

  JackTang(const JackTang &obj) { number = obj.number; }
  ~JackTang() {}

  void consumer() {
    for (int i = 0; i < 10; i++) {
      std::lock(m_utex1, m_utex2);  //两个锁一起锁住
      
      if (m_que.empty()) {
        cout << "m_que队列为空" << endl;
      } else {
        cout << "取出元素:" << m_que.front() << endl;
        m_que.pop();
      }

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  void productor() {
    for (int i = 0; i < 10; i++) {
      std::lock(m_utex1, m_utex2);  //两个锁一起锁住

      m_que.push(i);
      cout << "插入元素:" << i << endl;

      m_utex2.unlock();
      m_utex1.unlock();
    }
  }

  std::mutex m_utex1;
  std::mutex m_utex2;
  std::queue<int> m_que;
};

int main() {
  JackTang j(1);
  // 类中成员函数作为线程入口, 参考 2.2.其他创建线程的方法
  // 调用方式:对象成员函数地址,类实例,[成员函数参数]
  std::thread t1(&JackTang::consumer, &j);
  std::thread t2(&JackTang::productor, &j);

  t1.detach();
  t2.detach();
  std::this_thread::sleep_for(std::chrono::milliseconds(1));
  return 0;
}

运行结果:

m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
--------
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
取出元素:0
取出元素:1
取出元素:2
取出元素:3
取出元素:4
取出元素:5
取出元素:6
取出元素:7
取出元素:8
取出元素:9
--------
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9
取出元素:0
取出元素:1
取出元素:2
取出元素:3
取出元素:4
取出元素:5
取出元素:6
取出元素:7
取出元素:8
取出元素:9
--------
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
m_que队列为空
插入元素:0
插入元素:1
插入元素:2
插入元素:3
插入元素:4
插入元素:5
插入元素:6
插入元素:7
插入元素:8
插入元素:9

六.unique_lock(类模板)详解

请添加图片描述

6.1.unique_lock取代lock_guard

  • unique_lock比lock_guard灵活很多(多出来很多用法),效率差一点。
  • unique_lock<mutex> myUniLock(myMutex);
#include <iostream>
#include <list>
#include <mutex>
#include <thread>
using namespace std;

list<int> test_list;

mutex my_mutex;
void in_list() {
  for (int num = 0; num < 10; 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 < 10; ++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;
    } else {
      cout << "list数组为空" << 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;
}

运行结果:

插入数据: 0
插入数据: 1
插入数据: 2
插入数据: 3
插入数据: 4
插入数据: 5
插入数据: 6
插入数据: 7
插入数据: 8
插入数据: 9
取出数据:0
取出数据:1
取出数据:2
取出数据:3
取出数据:4
取出数据:5
取出数据:6
取出数据:7
取出数据:8
取出数据:9
Hello World!
--------
插入数据: 0
插入数据: 1
插入数据: 2
插入数据: 3
插入数据: 4
插入数据: 5
插入数据: 6
插入数据: 7
插入数据: 8
插入数据: 9
取出数据:0
取出数据:1
取出数据:2
取出数据:3
取出数据:4
取出数据:5
取出数据:6
取出数据:7
取出数据:8
取出数据:9
Hello World!
--------
插入数据: 0
插入数据: 1
插入数据: 2
插入数据: 3
插入数据: 4
插入数据: 5
插入数据: 6
插入数据: 7
插入数据: 8
插入数据: 9
取出数据:0
取出数据:1
取出数据:2
取出数据:3
取出数据:4
取出数据:5
取出数据:6
取出数据:7
取出数据:8
取出数据:9
Hello World!
--------
插入数据: 0
插入数据: 1
插入数据: 2
插入数据: 3
插入数据: 4
插入数据: 5
插入数据: 6
插入数据: 7
取出数据:0
取出数据:1
取出数据:2
取出数据:3
取出数据:4
取出数据:5
取出数据:6
取出数据:7
list数组为空
list数组为空
插入数据: 8
插入数据: 9
Hello World!

  • unique_lock可以取代lock_guard,实现加锁、自动释放锁的作用。

  • unique_lock不同于lock_guard的独特性,在于第二个参数的设置


6.2.unique_lock的第二个参数

6.2.1 std::adopt_lock:

  • 表示这个互斥锁已经被lock(),即不需要在构造函数中lock这个互斥锁了。
  • 前提:必须提前lock
  • lock_guard中也可以用这个参数
m_mutex.lock();
unique_lock<mutex> m_unique(m_mutex,adopt_lock);
、、、
//需要保护的数据
、、、

6.2.2 std::try_to_lock:

  • 尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
  • 使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
  • 前提:不能提前lock();
  • owns_locks()方法判断是否拿到锁,如拿到返回true
unique_lock<mutex> m_unique(m_mutex,try_to_lock);
if (m_unique.owns_lock()) {
  printf("拿到锁,容器插入数据\n", );
} else {
  printf("没有拿到锁,暂时不能插入数据\n" );
}
#include <iostream>
#include <list>
#include <mutex>
#include <thread>
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 << "list数组为空" << 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;
}

这个例子需要更多的数据观察,不贴结果了。

6.2.3 std::defer_lock:

  • 如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
  • 不给它加锁的目的是以后可以调用unique_lock的一些方法
  • 前提:不能提前lock

6.3.unique_lock的成员函数(前三个与std::defer_lock联合使用)

unique_lock的成员函数

  • lock(),加锁
  • unlock(),解锁;
  • try_lock(),尝试给互斥锁加锁,如果拿不到锁,则返回false,如果拿到了锁,返回true,这个函数不阻塞的;
  • release(),返回它所管理的lutext对象指针,并释放所有权﹔也就是说,这个unique_lock和mutex不再有关系。

6.3.1 lock():加锁。

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

不用自己unlock();

6.3.2 unlock():解锁。

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

因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()。

6.3.3 try_lock():尝试给互斥锁加锁

如果拿不到锁,返回false,否则返回true。

6.3.4 release():

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

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

  • a.锁住的代码少,叫做粒度细,执行效率高;
  • b.锁住的代码多,叫做粒度粗,执行效率低;

6.4.unique_lock所有权的传递

  • unique_lock<mutex> myUniLock(myMutex);把myMutex和myUniLock绑定在了一起,也就是myUniLock拥有myMutex的所有权

6.4.1. 使用move转移

  • myUniLock拥有myMutex的所有权,myUniLock可以把自己对myMutex的所有权转移,但是不能复制。
  • unique_lock myUniLock2(std::move(myUniLock));
    现在myUniLock2拥有myMutex的所有权。

6.4.2. 在函数中return一个临时变量,即可以实现转移

unique_lock<mutex> aFunction() {
    unique_lock<mutex> myUniLock(myMutex);
    //移动构造函数那里讲从函数返回一个局部的unique_lock对象是可以的
    //返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
    return myUniLock;
}
// 然后就可以在外层调用,在sbguard具有对myMutex的所有权
std::unique_lock<std::mutex> sbguard = aFunction();

七.单例设计模式共享数据分析、解决,call_once

请添加图片描述

7.1.设计模式

7.2.单例设计模式

  • 整个项目中,有某个或者某些特殊的类,只能创建一个属于该类的对象。
  • 单例类:只能生成一个对象。

7.3.单例设计模式共享数据分析、解决

  • 面临问题:需要在自己创建的线程中来创建单例类的对象,这种线程可能不止一个。可能面临GetInstance()这种成员函数需要互斥。
  • 可以在加锁前判断m_instance是否为空,否则每次调用Singleton::getInstance()都要加锁,十分影响效率。

这里讲得比较空洞,可以参考这篇博客:深入探索单例设计模式:以百度 Apollo 为例,3.2 多线程安全的懒汉式单例:单检锁实现小节介绍了多线程加锁。

#include <iostream>
#include <mutex>
using namespace std;

//饿汉模式
class HungrySingleton {
 private:
  static HungrySingleton* pinstance_;

 private:
  HungrySingleton() {}
  HungrySingleton(const HungrySingleton&) = delete;
  HungrySingleton& operator=(const HungrySingleton&) = delete;

 public:
  ~HungrySingleton() {}

 public:
  static HungrySingleton* GetInstance();
};

HungrySingleton* HungrySingleton::pinstance_ = new HungrySingleton;
HungrySingleton* HungrySingleton::GetInstance() { return pinstance_; }

// ---

// 懒汉式单例:单检锁实现
class LazySingleton {
 private:
  static LazySingleton* pinstance_;
  static std::mutex mutex_;

 private:
  LazySingleton() {}
  LazySingleton(const LazySingleton&) = delete;
  LazySingleton& operator=(const LazySingleton&) = delete;

 public:
  ~LazySingleton() {}

 public:
  static LazySingleton* GetInstance();
};

LazySingleton* LazySingleton::pinstance_{nullptr};
std::mutex LazySingleton::mutex_;

LazySingleton* LazySingleton::GetInstance() {
  // 加锁,为什么不用 uni
  // 每次调用 GetInstance 方法尝试获取实例时都会执行加锁操作,并在自析构
  // std::lock_guard 对象时执行解锁操作,这必然会降低实例访问效率
  std::lock_guard<std::mutex> lock(mutex_);
  if (nullptr == pinstance_) {
    pinstance_ = new LazySingleton;
  }
  return pinstance_;
}

class LazySingleton2 {
 private:
  static LazySingleton2* pinstance_;
  static std::mutex mutex_;

 private:
  LazySingleton2() {}
  LazySingleton2(const LazySingleton2&) = delete;
  LazySingleton2& operator=(const LazySingleton2&) = delete;

 public:
  ~LazySingleton2() {}

 public:
  static LazySingleton2* GetInstance();
};

LazySingleton2* LazySingleton2::pinstance_{nullptr};
std::mutex LazySingleton2::mutex_;

LazySingleton2* LazySingleton2::GetInstance() {
  if (nullptr == pinstance_) {
    // 双检锁模式(Double-Checked Locking Pattern,DCLP)优化方案,即在
    // GetInstance
    // 中执行锁操作前,在最外层额外地进行一次实例指针的检查操作(“双检”的体现)

    // 双检锁方法初衷虽好,但却破坏了多线程场景下的安全性,这是由动态内存分配时
    // new 底层操作的非原子性导致的
    std::lock_guard<std::mutex> lock(mutex_);
    if (nullptr == pinstance_) {
      pinstance_ = new LazySingleton2;
    }
  }
  return pinstance_;
}

int main(void) {
  LazySingleton2* singer = LazySingleton2::GetInstance();
  LazySingleton2* singer2 = LazySingleton2::GetInstance();
  if (singer == singer2)
    cout << "二者是同一个实例" << endl;
  else
    cout << "二者不是同一个实例" << endl;

  cout << "---- 以下是饿汉式 ----" << endl;
  HungrySingleton* singer3 = HungrySingleton::GetInstance();
  HungrySingleton* singer4 = HungrySingleton::GetInstance();
  if (singer3 == singer4)
    cout << "二者是同一个实例" << endl;
  else
    cout << "二者不是同一个实例" << endl;

  return 0;
}

如果觉得在单例模式new了一个对象,而没有自己delete掉,这样不合理。可以增加一个类中类CGarhuishou,new一个单例类时创建一个静态的CGarhuishou对象,这样在程序结束时会调用CGarhuishou的析构函数,释放掉new出来的单例对象。

#include <iostream>
#include <mutex>
using namespace std;

class LazySingleton2 {
 private:
  static LazySingleton2* pinstance_;
  static std::mutex mutex_;

 private:
  LazySingleton2() {}
  LazySingleton2(const LazySingleton2&) = delete;
  LazySingleton2& operator=(const LazySingleton2&) = delete;

 public:
  ~LazySingleton2() {}

 public:
  static LazySingleton2* GetInstance();
  class CGarhuishou {
   public:
    ~CGarhuishou() {
      if (LazySingleton2::pinstance_) {
        delete LazySingleton2::pinstance_;
        LazySingleton2::pinstance_ = NULL;
      }
    }
  };
};

LazySingleton2* LazySingleton2::pinstance_{nullptr};
std::mutex LazySingleton2::mutex_;

LazySingleton2* LazySingleton2::GetInstance() {
  if (nullptr == pinstance_) {
    // 双检锁模式(Double-Checked Locking Pattern,DCLP)优化方案,即在
    // GetInstance
    // 中执行锁操作前,在最外层额外地进行一次实例指针的检查操作(“双检”的体现)

    // 双检锁方法初衷虽好,但却破坏了多线程场景下的安全性,这是由动态内存分配时
    // new 底层操作的非原子性导致的
    std::lock_guard<std::mutex> lock(mutex_);
    if (nullptr == pinstance_) {
      static CGarhuishou huishou;
      pinstance_ = new LazySingleton2;
    }
  }
  return pinstance_;
}

int main(void) {
  LazySingleton2* singer = LazySingleton2::GetInstance();
  LazySingleton2* singer2 = LazySingleton2::GetInstance();
  if (singer == singer2)
    cout << "二者是同一个实例" << endl;
  else
    cout << "二者不是同一个实例" << endl;
  return 0;
}
  • 参考博客中还提到了使用原子变量确保多线程安全性,后续再看。

7.4.std::once_flag & std::call_once()

  • std::call_once()函数模板,该函数的第一个参数为标记,第二个参数是一个函数名(如a())。

  • 功能:能够保证函数a()只被调用一次。具备互斥锁的能力,而且比互斥锁消耗的资源更少,更高效。

  • call_once()需要与一个标记结合使用,这个标记为std::once_flag;其实once_flag是一个结构,call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。

  • 多个线程同时执行时,一个线程会等待另一个线程先执行。

#include <iostream>
#include <mutex>
using namespace std;

std::once_flag g_flag;
class Singleton {
 public:
  // call_once保证其只被调用一次
  static void CreateInstance() { instance = new Singleton; }
  //两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕
  static Singleton* getInstance() {
    std::call_once(g_flag, CreateInstance);
    return instance;
  }

 private:
  Singleton() {}
  static Singleton* instance;
};
Singleton* Singleton::instance = NULL;

int main(void) {
  Singleton* singer = Singleton::getInstance();
  Singleton* singer2 = Singleton::getInstance();
  if (singer == singer2)
    cout << "二者是同一个实例" << endl;
  else
    cout << "二者不是同一个实例" << endl;
  return 0;
}

apollo中的单例模式:

#include <iostream>
#include <memory>
#include <mutex>
#include <type_traits>
#include <utility>
#include <thread>

#define DEFINE_TYPE_TRAIT(name, func)                      \
  template <typename T>                                    \
  struct name {                                            \
    template <typename Class>                              \
    static constexpr bool Test(decltype(&Class::func) *) { \
      return true;                                         \
    }                                                      \
    template <typename>                                    \
    static constexpr bool Test(...) {                      \
      return false;                                        \
    }                                                      \
                                                           \
    static constexpr bool value = Test<T>(nullptr);        \
  };                                                       \
                                                           \
  template <typename T>                                    \
  constexpr bool name<T>::value;

DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)

template <typename T>
typename std::enable_if<HasShutdown<T>::value>::type CallShutdown(T *instance) {
  instance->Shutdown();
}

template <typename T>
typename std::enable_if<!HasShutdown<T>::value>::type CallShutdown(
    T *instance) {
  (void)instance;
}

// There must be many copy-paste versions of these macros which are same
// things, undefine them to avoid conflict.
#undef UNUSED
#undef DISALLOW_COPY_AND_ASSIGN

#define UNUSED(param) (void)param

#define DISALLOW_COPY_AND_ASSIGN(classname) \
  classname(const classname &) = delete;    \
  classname &operator=(const classname &) = delete;

#define DECLARE_SINGLETON(classname)                                                   \
 public:                                                                               \
  static classname *Instance(bool create_if_needed = true) {                           \
    /*实例的唯一性通过局部静态(local static)的实例指针实现:*/ \
    static classname *instance = nullptr;                                              \
    if (!instance && create_if_needed) {                                               \
      /*多线程安全性由 std::once_flag 和 std::call_once 保证:*/              \
      static std::once_flag flag;                                                      \
      std::call_once(flag,                                                             \
                     [&] { instance = new (std::nothrow) classname(); });              \
    }                                                                                  \
    return instance;                                                                   \
  }                                                                                    \
                                                                                       \
  static void CleanUp() {                                                              \
    auto instance = Instance(false);                                                   \
    if (instance != nullptr) {                                                         \
      CallShutdown(instance);                                                          \
    }                                                                                  \
  }                                                                                    \
                                                                                       \
 private:                                                                              \
  classname();                                                                         \
  /*防止私自创建实例,通过私有化默认构造函数 &                       \
   * 禁止拷贝构造和拷贝赋值的宏定义实现:*/                          \
  DISALLOW_COPY_AND_ASSIGN(classname)


// -----------
/**
 * @brief Singleton class without `Shutdown` method.
 */
class SingletonA {
 private:
  ~SingletonA() = default;

 private:
  static int num;

 public:
  static void GetNum() {
    std::cout << "\n number of instances of SingletonA: " << num << std::endl;
  }

  DECLARE_SINGLETON(SingletonA)
};

int SingletonA::num = 0;
SingletonA::SingletonA() { ++num; }


// -----------
/**
 * @brief Singleton class with `Shutdown` method.
 */
class SingletonB {
 private:
  ~SingletonB() = default;

 private:
  static int num;

 public:
  // `Shutdown` method should be declared as `public` for type traits
  void Shutdown();

  static void GetNum() {
    std::cout << "\n number of instances of SingletonB: " << num << std::endl;
  }

  DECLARE_SINGLETON(SingletonB)
};

int SingletonB::num = 0;
SingletonB::SingletonB() { ++num; }

void SingletonB::Shutdown() {
  auto instance = Instance(false);
  if (instance != nullptr) {
    delete instance;
    num = 0;
  }
  std::cout << "\n SingletonB::Shutdown method was called." << std::endl;
}

template <typename T>
void ThreadFunc() {
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  T *p = T::Instance();
}

int main() {
  std::thread tA1(ThreadFunc<SingletonA>);
  std::thread tA2(ThreadFunc<SingletonA>);
  std::thread tB1(ThreadFunc<SingletonB>);
  std::thread tB2(ThreadFunc<SingletonB>);

  tA1.join();
  tA2.join();
  tB1.join();
  tB2.join();

  SingletonA::GetNum();
  SingletonB::GetNum();

  SingletonA::CleanUp();
  SingletonB::CleanUp();

  SingletonA::GetNum();
  SingletonB::GetNum();

  return 0;
}

运行结果:

 number of instances of SingletonA: 1

 number of instances of SingletonB: 1

 SingletonB::Shutdown method was called.

 number of instances of SingletonA: 1

 number of instances of SingletonB: 0

八.condition_variable、wait、notify_one、notify_all

请添加图片描述

8.1.条件变量condition_variable、wait、notify_one、notify_all

std::condition_variable实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。wait为condition_variable成员函数。

std::mutex mymutex1;
std::unique_lock<std::mutex> sbguard1(mymutex1);
std::condition_variable condition;
condition.wait(sbguard1, [this] {
  if (!msgRecvQueue.empty()) return true;
  return false;
});
  • wait()用来等一个东西

  • 如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行。

  • 如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥锁,并阻塞到本行

    • 阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()成员函数为止;
  • 如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样

    • wait()将解锁互斥锁,并阻塞到本行,阻塞到其他某个线程调用notify_one()成员函数为止。
  • 当其他线程用notify_one() 将本wait(原来是睡着/阻塞)的状态唤醒后,wait就开始恢复干活了,恢复后wait干什么活?

    • a) wait() 不断的尝试重新获取互斥锁锁,如果获取不到,那么流程就卡在wait这里等着获取,如果获取到了,那么wait就走下来。
    • b1)如果wait有第二个参数(lambda),就判断这个lambda表达式,如果lambda表达式为false,那wait又对互斥锁解锁,然后又休眠这里等待再次被notify_one唤醒。
    • b2) 如果lambda表达式为true,则wait返回,流程走下来。(此时互斥锁被锁着)
    • b3) 如果wait没有第二个参数,则wait返回,流程走下来。
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <list>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace std;

class A {
 public:
  //把收到的消息(玩家命令)加入到一个队列的线程
  void inMsgRecvQueue() {
    for (int i = 1; i < 10; ++i) {
      cout << "inMsgRecvQueue执行了,插入一个元素" << i << endl;
      std::unique_lock<mutex> in_mutex_guard(my_mutex);
      //假设这个数字就是玩家发来的命令,加入到消息队列中
      msgRecvQueue.push_back(i);
      //尝试吧wait()唤醒,执行完这行,outMsgRecvQueue中的wait被唤醒
      cond_var.notify_one();
      //假如outMsgRecvQueue()正在处理一个事务,需要一段时间,
      //而不是正卡在wait()那里等待你唤醒,那么此时这个notify_one()这个调用也许就没效果.
    }
  }

  //把消息从消息队列中取出的线程
  void outMsgRecvQueue() {
    int command{};
    while (true) {
      std::unique_lock<mutex> outMutex(my_mutex);
      // wait用来等一个东西
      cond_var.wait(outMutex, [this]() {
        if (!msgRecvQueue.empty()) return true;
        return false;
      });

      // 流程只要能走到这里来,这个互斥锁一定是锁着的。同时msgRecvQueue至少是有一条数据的。
      // 返回第一个元素,但不检查元素是否存在
      command = msgRecvQueue.front();
      //移除第一个元素,但不返回
      msgRecvQueue.pop_front();
      //因为unique_lock的灵活性,所以我们可以随时的unlock解锁,以免锁住太长时间
      outMutex.unlock();
      cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
    }  // end while
  }    // end outMsgRecvQueue()

 private:
  std::list<int> msgRecvQueue;  //容器(消息队列),专门代表玩家给我们发来的命令
  std::mutex my_mutex;
  std::condition_variable cond_var;  //生成一个条件变量对象
};                                   // end A

int main() {
  A obja;
  //第二个参数是引用,保证线程里操作同一个对象
  std::thread outMsgThread(&A::outMsgRecvQueue, &obja);
  std::thread inMsgThread(&A::inMsgRecvQueue, &obja);

  inMsgThread.detach();
  outMsgThread.detach();
  //主线程执行
  std::this_thread::sleep_for(std::chrono::milliseconds(1));

  std::cout << "主线程结束" << std::endl;
  return 0;
}

运行结果:

inMsgRecvQueue执行了,插入一个元素2
inMsgRecvQueue执行了,插入一个元素3
inMsgRecvQueue执行了,插入一个元素4
inMsgRecvQueue执行了,插入一个元素5
inMsgRecvQueue执行了,插入一个元素6
inMsgRecvQueue执行了,插入一个元素7
inMsgRecvQueue执行了,插入一个元素8
inMsgRecvQueue执行了,插入一个元素9
outMsgRecvQueue()执行,取出一个元素1
outMsgRecvQueue()执行,取出一个元素2
outMsgRecvQueue()执行,取出一个元素3
outMsgRecvQueue()执行,取出一个元素4
outMsgRecvQueue()执行,取出一个元素5
outMsgRecvQueue()执行,取出一个元素6
outMsgRecvQueue()执行,取出一个元素7
outMsgRecvQueue()执行,取出一个元素8
outMsgRecvQueue()执行,取出一个元素9
主线程结束
  • wait()不断尝试获取互斥锁锁,如果获取不到那么流程就卡在wait()这里等待获取,如果获取到了,那么wait()就继续执行,获取到了锁

1、如果wait有第二个参数就判断这个lambda表达式。

  • a)如果表达式为false,那wait又对互斥锁解锁,然后又休眠,等待再次被notify_one()唤醒
  • b)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥锁已被锁住)。

2、如果wait没有第二个参数,则wait返回,流程走下去。

流程只要走到了wait()下面则互斥锁一定被锁住了。

8.2.深入思考

上面的代码可能导致出现一种情况:

因为outMsgRecvQueue()与inMsgRecvQueue()并不是一对一执行的,所以当程序循环执行很多次以后,可能在msgRecvQueue 中已经有了很多消息,但是,outMsgRecvQueue还是被唤醒一次只处理一条数据。这时可以考虑把outMsgRecvQueue多执行几次,或者对inMsgRecvQueue进行限流。

8.3.notify_all()

  • notify_one():通知一个线程的wait()

  • notify_all():通知所有线程的wait()

九.async、future、packaged_task、promise

请添加图片描述

本节内容需要包含头文件#include <future>

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

  • std::async是一个函数模板,用来启动一个异步任务,启动一个异步任务之后,它返回一个std::future对象,这个对象是个类模板。

  • 什么叫“启动一个异步任务”?就是自动创建一个线程,并开始 执行对应的线程入口函数,它返回一个std::future对象,这个std::future对象中就含有线程入口函数所返回的结果,我们可以通过调用future对象的成员函数get()来获取结果。

  • “future”将来的意思,也有人称呼std::future提供了一种访问异步操作结果的机制,就是说这个结果你可能没办法马上拿到,但是在不久的将来,这个线程执行完毕的时候,你就能够拿到结果了,所以,大家这么理解:future中保存着一个值,这个值是在将来的某个时刻能够拿到。

  • std::future对象的get()成员函数会等待线程执行结束并返回结果,拿不到结果它就会一直等待,感觉有点像join()。但是,它是可以获取结果的。

  • std::future对象的wait()成员函数,用于等待线程返回,本身并不返回结果,这个效果和 std::thread 的join()更像。


std::async可以看成是 std::threads 的一个进阶

假设我们要执行另外一个函数work:

bool work ( int x);

使用 std::async 调用 work(123) 函数的方法为:

std::future< bool > fut = std::async (wrok, 123 );

调用 std::async 之后会立即传回一个 std::future 的结果ret,随后即可通过这个结果取得计算结果:

bool r = fut.get();

这就是 std::async 最简单的用法。

例子,检查一个整数是否为质数:

#include <future>
#include <iostream>

// 耗时的工作
bool is_prime(int x) {
  std::cout << "Calculating. Please, wait... \n";
  bool r = true;
  for (int i = 2; i < x; ++i) {
    if (x % i == 0) {
      r = false;
      break;
    }
  }
  std::cout << "We're done. \n";
  return r;
}

// sleep 函数
void sleep(int milisec) {
  std::this_thread::sleep_for(std::chrono::milliseconds(milisec));
}

int main() {
  // 并行执行is_prime(334214467)
  std::future<bool> fut = std::async(is_prime, 334214467);

  // 执行其他工作...
  sleep(200);
  std::cout << "Do something in main thread ... \n";
  sleep(200);
  std::cout << "Do something in main thread ... \n";
  sleep(200);
  std::cout << "Do something in main thread ... \n";

  // 等待并取回计算结果
  bool r = fut.get();
  if (r) {
    std::cout << "It is prime! \n";
  } else {
    std::cout << "It is not prime. \n";
  }
  return 0;
}

运行结果:

Calculating. Please, wait... 
Do something in main thread ... 
Do something in main thread ... 
Do something in main thread ... 
We're done. 
It is prime! 

例子:

#include <future>
#include <iostream>
using namespace std;
class A {
 public:
  int mythread(int mypar) {
    cout << mypar << endl;
    return mypar;
  }
};

int mythread() {
  cout << "mythread() start"
       << " thread id = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << " thread id = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  A a;
  int tmp = 12;
  cout << "main"
       << " thread id = " << std::this_thread::get_id() << endl;
  std::future<int> result1 = std::async(mythread);
  cout << "continue........" << endl;
  cout << result1.get() << endl;  //卡在这里等待mythread()执行完毕,拿到结果

  //类成员函数
  //第二个参数是对象引用才能保证线程里执行的是同一个对象
  std::future<int> result2 = std::async(&A::mythread, &a, tmp);
  cout << result2.get() << endl;
  //或者result2.wait();
  cout << "good luck" << endl;
  return 0;
}

运行结果:

main thread id = 140627844208448
continue........
mythread() start thread id = 140627802838784
mythread() end thread id = 140627802838784
5
12
12
good luck

我们通过向std::async()传递一个参数,该参数是std::launch类型(枚举类型),来达到一些特殊的目的:

9.1.1.std::async 的两种执行模式之std::launch::async

  • std::launch::async并行执行
    这种方式就是另外开启一个线程,以平行的方式执行,这个应该是大部分人所期望的模式。

请添加图片描述

请添加图片描述

#include <future>
#include <iostream>
using namespace std;

int mythread() {
  cout << "mythread() start"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  cout << "main"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::future<int> result1 = std::async(std::launch::async, mythread);
  cout << "continue........" << endl;
  cout << result1.get() << endl;  //卡在这里等待mythread()执行完毕,拿到结果
  cout << "good luck" << endl;
  return 0;
}

运行结果:

mainthreadid = 140706613393216
continue........
mythread() startthreadid = 140706572023552
mythread() endthreadid = 140706572023552
5
good luck

9.1.2.std::async 的两种执行模式之std::launch::deferred

  • std::launch::deferred延后执行
    这种方式就是很单纯的将函数调用的时间延后,当 std::future 调用 get() 时,才去真正执行其中的函数,如果wait()或者get()没有被调用,则不会执行,而执行的方式就是一般的函数调用,没有任何平行化效果。

请添加图片描述

#include <future>
#include <iostream>
using namespace std;

int mythread() {
  cout << "mythread() start"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  cout << "main"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::future<int> result1 = std::async(std::launch::deferred, mythread);
  cout << "continue........" << endl;
  cout << result1.get() << endl;  //卡在这里等待mythread()执行完毕,拿到结果
  cout << "good luck" << endl;
  return 0;
}

运行结果:

mainthreadid = 140539030345536
continue........
mythread() startthreadid = 140539030345536
mythread() endthreadid = 140539030345536
5
good luck

永远都会先打印出continue…,然后才会打印出mythread() start和mythread() end等信息。

9.2.std::packaged_task:打包任务,把任务包装起来

类模板,它的模板参数是各种可调用对象,通过packaged_task把各种可调用对象包装起来,方便将来作为线程入口函数来调用。

请添加图片描述

#include <future>
#include <iostream>
#include <thread>
using namespace std;

int mythread(int mypar) {
  cout << mypar << endl;
  cout << "mythread() start"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  cout << "main"
       << "threadid = " << std::this_thread::get_id() << endl;
  //我们把函数mythread通过packaged_task包装起来
  //参数是一个int,返回值类型是int
  std::packaged_task<int(int)> mypt(mythread);
  std::thread t1(std::ref(mypt), 1);
  t1.join();
  std::future<int> result = mypt.get_future();
  // std::future对象里包含有线程入口函数的返回结果,这里result保存mythread返回的结果。
  cout << result.get() << endl;

  return 0;
}

运行结果:

mainthreadid = 139888845096768
1
mythread() startthreadid = 139888803727104
mythread() endthreadid = 139888803727104
5
  • 可调用对象可由函数换成lambda表达式
#include <future>
#include <iostream>
#include <thread>
using namespace std;

int mythread(int mypar) {
  cout << mypar << endl;
  cout << "mythread() start"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  cout << "main"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::packaged_task<int(int)> mypt([](int mypar) {
    cout << mypar << endl;
    cout << "mythread() start"
         << "threadid = " << std::this_thread::get_id() << endl;
    std::chrono::milliseconds dura(5000);
    std::this_thread::sleep_for(dura);
    cout << "mythread() end"
         << "threadid = " << std::this_thread::get_id() << endl;
    return 5;
  });

  std::thread t1(std::ref(mypt), 1);
  t1.join();
  std::future<int> result = mypt.get_future();
  // std::future对象里包含有线程入口函数的返回结果,这里result保存mythread返回的结果。
  cout << result.get() << endl;
  cout << "good luck" << endl;
  return 0;
}

packaged_task包装起来的可调用对象还可以直接调用,从这个角度来讲,packaged_task对象也是一个可调用对象

运行结果:

mainthreadid = 140083709134656
1
mythread() startthreadid = 140083667764992
mythread() endthreadid = 140083667764992
5
good luck
  • lambda的直接调用
#include <future>
#include <iostream>
#include <thread>
using namespace std;

int mythread(int mypar) {
  cout << mypar << endl;
  cout << "mythread() start"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}
int main() {
  cout << "main"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::packaged_task<int(int)> mypt([](int mypar) {
    cout << mypar << endl;
    cout << "mythread() start"
         << "threadid = " << std::this_thread::get_id() << endl;
    std::chrono::milliseconds dura(5000);
    std::this_thread::sleep_for(dura);
    cout << "mythread() end"
         << "threadid = " << std::this_thread::get_id() << endl;
    return 5;
  });

  mypt(1);
  std::future<int> result = mypt.get_future();
  cout << result.get() << endl;
}

运行结果:

mainthreadid = 139697186420544
1
mythread() startthreadid = 139697186420544
mythread() endthreadid = 139697186420544
5
  • std::promise,类模板
    我们能够在某个线程中给它赋值,然后我们可以在其他线程中,把这个值取出来

请添加图片描述

#include <future>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;

void mythread(std::promise<int> &tmp, int clac) {
  cout << "mythread() start"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << "threadid = " << std::this_thread::get_id() << endl;
  int result = clac;
  tmp.set_value(result);  //结果保存到了tmp这个对象中
  return;
}

vector<std::packaged_task<int(int)>> task_vec;

int main() {
  std::promise<int> myprom;
  std::thread t1(mythread, std::ref(myprom), 180);
  t1.join();  //在这里线程已经执行完了
  std::future<int> fu1 =
      myprom.get_future();  // promise和future绑定,用于获取线程返回值
  auto result = fu1.get();
  cout << "result = " << result << endl;
}

运行结果:

mythread() startthreadid = 140174314473216
mythread() endthreadid = 140174314473216
result = 180
  • 总结:通过promise保存一个值,在将来某个时刻我们通过把一个future绑定到这个promise上,来得到绑定的值

  • 注意:使用thread时,必须 join() 或者 detach() 否则程序会报异常

  • 小结:

    • 我们学习这些东西的目的并不是,要把他们都用到实际开发中。
    • 相反,如果我们能够用最少的东西写出一个稳定的,高效的多线程程序,更值得赞赏。
    • 我们为了成长必须阅读一些高手写的代码,从而实现自己代码的积累;

十.future其他成员函数、shared_future、atomic

future成员函数:

  • 获取结果

    • get()
  • 状态

    • valid : 检查 future 是否拥有共享状态
    • wait : 等待结果变得可用
    • wait_for : 等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回。
    • wait_until : 等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回。

请添加图片描述

10.1.std::future 的成员函数

  • 1、std::future_status status = result.wait_for(std::chrono::seconds(几秒));
    • 卡住当前流程,等待std::async()的异步任务运行一段时间,然后返回其状态std::future_status。如果std::async()的参数是 std::launch::deferred(延迟执行),则不会卡住主流程。
    • std::future_status是枚举类型,表示异步任务的执行状态。类型的取值有
      • std::future_status::timeout 共享状态在经过指定的等待时间内仍未就绪
      • std::future_status::ready 共享状态就绪
      • std::future_status::deferred 共享状态持有的函数正在延迟运行,结果将仅在显式请求时计算

wait_for使用例子

#include <future>
#include <iostream>
using namespace std;

int mythread() {
  cout << "mythread() start"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end"
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  cout << "main"
       << "threadid = " << std::this_thread::get_id() << endl;
  std::future<int> result = std::async(mythread);
  cout << "continue........" << endl;
  // cout << result1.get() << endl; //卡在这里等待mythread()执行完毕,拿到结果
  //等待1秒
  std::future_status status = result.wait_for(std::chrono::seconds(1));
  if (status == std::future_status::timeout) {
    //超时:表示线程还没有执行完
    cout << "超时了,线程还没有执行完" << endl;
  }

  return 0;
}

运行结果:

mainthreadid = 140610447968064
continue........
mythread() startthreadid = 140610406598400
超时了,线程还没有执行完
mythread() endthreadid = 140610406598400
#include <future>
#include <iostream>
using namespace std;

int mythread() {
  cout << "mythread() start "
       << "threadid = " << std::this_thread::get_id() << endl;
  // std::chrono::milliseconds dura(5000);
  // std::this_thread::sleep_for(dura);
  cout << "mythread() end "
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  cout << "main "
       << "threadid = " << std::this_thread::get_id() << endl;
  // std::future<int> result = std::async(std::launch::deferred, mythread);
  std::future<int> result = std::async(mythread);
  cout << "continue........" << endl;
  // cout << result1.get() << endl; //卡在这里等待mythread()执行完毕,拿到结果
  std::future_status status = result.wait_for(std::chrono::seconds(6));
  if (status == std::future_status::timeout) {
    //超时:表示线程还没有执行完
    cout << "超时了,线程还没有执行完" << endl;
  } else if (status == std::future_status::ready) {
    //表示线程成功返回
    cout << "线程执行成功,返回" << endl;
    cout << result.get() << endl;
  } else if (status == std::future_status::deferred) {
    //如果设置 std::future<int> result = std::async(std::launch::deferred,
    // mythread);,则本条件成立
    cout << "线程延迟执行" << endl;
    cout << result.get() << endl;
  }

  cout << "good luck" << endl;
  return 0;
}

运行结果:

main threadid = 140209946228544
continue........
mythread() start threadid = 140209904858880
mythread() end threadid = 140209904858880
线程执行成功,返回
5
good luck

get()只能使用一次,比如如果

auto a = result.get();
cout << result.get() << endl;

就会报告异常
因为get()函数的设计是一个移动语义,相当于将result中的值移动到了a中,再次get就报告了异常。

10.2.std::shared_future:也是个类模板

  • std::future的 get() 成员函数是转移数据
  • std::shared_future 的 get()成员函数是复制数据
#include <future>
#include <iostream>
#include <thread>
using namespace std;

int mythread() {
  cout << "mythread() start "
       << "threadid = " << std::this_thread::get_id() << endl;
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  cout << "mythread() end "
       << "threadid = " << std::this_thread::get_id() << endl;
  return 5;
}

int main() {
  cout << "main "
       << "threadid = " << std::this_thread::get_id() << endl;
  std::packaged_task<int()> mypt(mythread);
  std::thread t1(std::ref(mypt));
  std::future<int> result = mypt.get_future();

  bool ifcanget = result.valid();  //判断future中的值是不是一个有效值
  std::shared_future<int> result_s(
      result.share());  //执行完毕后result_s里有值,而result里空了
                        // std::shared_future<int> result_s(std::move(result));
  //通过get_future返回值直接构造一个shared_future对象
  // std::shared_future<int> result_s(mypt.get_future());
  t1.join();

  auto myresult1 = result_s.get();
  auto myresult2 = result_s.get();

  cout << "good luck" << endl;
  return 0;
}

运行结果:

main threadid = 140029196932928
mythread() start threadid = 140029155563264
mythread() end threadid = 140029155563264
good luck

10.3.std::atomic原子操作

10.3.1 原子操作概念引出范例:

  • 互斥锁:多线程编程中 用于保护共享数据:先锁住, 操作共享数据, 解锁。

  • 有两个线程,对一个变量进行操作,一个线程读这个变量的值,一个线程往这个变量中写值。

  • 即使是一个简单变量的读取和写入操作,如果不加锁,也有可能会导致读写值混乱(一条C语句会被拆成3、4条汇编语句来执行,所以仍然有可能混乱)

#include <iostream>
#include <thread>
using namespace std;
int g_count = 0;
 
void mythread1() {
	for (int i = 0; i < 1000000; i++) {
		g_count++;
	}
}
 
int main() {
	std::thread t1(mythread1);
	std::thread t2(mythread1);
	t1.join();
	t2.join();
	cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
}

我这里测试的这个用例结果是正确的,没有混乱:

正常情况下结果应该是200 00000次,实际是20000000

使用mutex解决这个问题

#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
int g_count = 0;
std::mutex mymutex;

void mythread1() {
  for (int i = 0; i < 1000000; i++) {
    std::unique_lock<std::mutex> u1(mymutex);
    g_count++;
  }
}

int main() {
  std::thread t1(mythread1);
  std::thread t2(mythread1);
  t1.join();
  t2.join();
  cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
}

10.3.2 基本的std::atomic用法范例

  • 大家可以把原子操作理解成一种:不需要用到互斥锁加锁(无锁)技术的多线程并发编程方式

  • 原子操作:在多线程中不会被打断的程序执行片段。

  • 从效率上来说,原子操作要比互斥锁的方式效率要高。

  • 互斥锁的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。

  • 原子操作,一般都是指“不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的,不可能出现半完成状态

  • std::atomic来代表原子操作,是个类模板。其实std::atomic是用来封装某个类型的值的

  • 需要添加#include <atomic>头文件

范例:

#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
std::atomic<int> g_count(0);  //封装了一个类型为int的 对象(值)

void mythread1() {
  for (int i = 0; i < 1000000; i++) {
    g_count++;
  }
}

int main() {
  std::thread t1(mythread1);
  std::thread t2(mythread1);
  t1.join();
  t2.join();
  cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
}
#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
std::atomic<bool> g_ifEnd(false);  //封装了一个类型为bool的 对象(值)

void mythread() {
  std::chrono::milliseconds dura(1000);
  while (g_ifEnd == false) {
    cout << "thread id = " << std::this_thread::get_id() << "运行中" << endl;
    std::this_thread::sleep_for(dura);
  }
  cout << "thread id = " << std::this_thread::get_id() << "运行结束" << endl;
}

int main() {
  std::thread t1(mythread);
  std::thread t2(mythread);
  std::chrono::milliseconds dura(5000);
  std::this_thread::sleep_for(dura);
  g_ifEnd = true;
  cout << "程序执行完毕" << endl;
  t1.join();
  t2.join();
}

运行结果:

thread id = 139752726234880运行中
thread id = 139752734627584运行中
thread id = thread id = 139752734627584运行中139752726234880
运行中
thread id = thread id = 139752734627584运行中139752726234880
运行中
thread id = thread id = 139752734627584139752726234880运行中运行中

thread id = thread id = 139752726234880139752734627584运行中运行中

程序执行完毕
thread id = 139752726234880运行结束
thread id = 139752734627584运行结束

总结:

  • 1、原子操作一般用于计数或者统计(如累计发送多少个数据包,累计接收到了多少个数据包),多个线程一起统计,这种情况如果不使用原子操作会导致统计发生混乱。

  • 2、写商业代码时,如果不确定结果的影响,最好自己先写一小段代码调试。或者不要使用。

11.std::atomic续谈、std::async深入谈

请添加图片描述

11.1.std::atomic续谈

#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
std::atomic<int> g_count(0);  //封装了一个类型为int的 对象(值)

void mythread1() {
  for (int i = 0; i < 1000000; i++) {
    //虽然g_count使用了原子操作模板,但是这种写法既读又写,
    //会导致计数错误
    g_count = g_count + 1;
  }
}

int main() {
  std::thread t1(mythread1);
  std::thread t2(mythread1);
  t1.join();
  t2.join();
  cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
}

运行结果:

------------
正常情况下结果应该是200 0000次,实际是1463947
------------
正常情况下结果应该是200 0000次,实际是1030510
------------
正常情况下结果应该是200 0000次,实际是1166023

一般atomic原子操作,针对++,–,+=,-=,&=,|=,^=是支持的,其他操作不一定支持

11.2.std::async深入理解

11.2.1 std::async参数详述,async 用来创建一个异步任务

  • 延迟调用参数 std::launch::deferred【延迟调用】,std::launch::async【强制创建一个线程】

  • std::async()我们一般不叫创建线程(他能够创建线程),我们一般叫它创建一个异步任务

  • std::async和std::thread最明显的不同,就是 async 有时候并不创建新线程。


  • ①如果用std::launch::deferred 来调用async?

    • 延迟到调用 get() 或者 wait() 时执行,如果不调用就不会执行
  • ②如果用std::launch::async来调用async?

    • 强制这个异步任务在新线程上执行,这意味着,系统必须要创建出新线程来运行入口函数。
  • ③如果同时用 std::launch::async | std::launch::deferred

    • 这里这个 | 意味着async的行为可能是 std::launch::async 创建新线程立即执行, 也可能是 std::launch::deferred 没有创建新线程并且延迟到调用get()执行,由系统根据实际情况来决定采取哪种方案
  • ④不带额外参数 std::async(mythread),只给async 一个入口函数名,此时的系统给的默认值是 std::launch::async | std::launch::deferred 和 ③ 一样,有系统自行决定异步还是同步运行。

11.2.2 std::async和std::thread()区别:

  • std::thread()如果系统资源紧张可能出现创建线程失败的情况,如果创建线程失败那么程序就可能崩溃,而且不容易拿到函数返回值(不是拿不到)
  • std::async()创建异步任务。可能创建线程也可能不创建线程,并且容易拿到线程入口函数的返回值;

由于系统资源限制:

  • ①如果用std::thread创建的线程太多,则可能创建失败,系统报告异常,崩溃。

  • ②如果用std::async,一般就不会报异常,因为如果系统资源紧张,无法创建新线程的时候,async不加额外参数的调用方式就不会创建新线程。而是在后续调用get()请求结果时执行在这个调用get()的线程上。

    • 如果你强制async一定要创建新线程就要使用 std::launch::async 标记。承受的代价是,系统资源紧张时可能崩溃。
  • ③根据经验,一个程序中线程数量 不宜超过100~200 。

11.2.3 async不确定性问题的解决

  • 不加额外参数的async调用时让系统自行决定,是否创建新线程。

  • std::future<int> result = std::async(mythread);
    问题焦点在于这个写法,任务到底有没有被推迟执行。

  • 通过wait_for返回状态来判断:

std::future_status status = result.wait_for(std::chrono::seconds(6));
// std::future_status status = result.wait_for(6s);
if (status == std::future_status::timeout) {
  //超时:表示线程还没有执行完
  cout << "超时了,线程还没有执行完" << endl;
} else if (status == std::future_status::ready) {
  //表示线程成功放回
  cout << "线程执行成功,返回" << endl;
  cout << result.get() << endl;
} else if (status == std::future_status::deferred) {
  cout << "线程延迟执行" << endl;
  cout << result.get() << endl;
}

十二. windows临界区、其他各种mutex互斥锁

请添加图片描述

12.1&2.windows临界区

  • Windows临界区,同一个线程是可以重复进入的,但是进入的次数与离开的次数必须相等。
    C++互斥锁则不允许同一个线程重复加锁。

  • windows临界区是在windows编程中的内容,了解一下即可,效果几乎可以等同于c++11的mutex

  • 包含#include <windows.h>
    windows中的临界区同mutex一样,可以保护一个代码段。但windows的临界区可以进入多次,离开多次,但是进入的次数与离开的次数必须相等,不会引起程序报异常出错。

略。

12.3.自动析构技术

  • C++:lock_guard防止忘了释放信号量,自动释放
  • windows:可以写个类自动释放临界区: 略

12.4.递归独占互斥锁 std::recursive_mutex

  • std::mutex 独占式互斥锁

  • std::recursive_mutex:允许在同一个线程中同一个互斥锁多次被 lock() ,(但是递归加锁的次数是有限制的,太多可能会报异常),效率要比mutex低。

  • 如果你真的用了 recursive_mutex 要考虑代码是否有优化空间,如果能调用一次 lock()就不要调用多次。

12.5.带超时的互斥锁 std::timed_mutex 和 std::recursive_timed_mutex

  • 5.1 std::timed_mutex:是待超时的独占互斥锁

try_lock_for():
等待一段时间,如果拿到了锁,或者超时了未拿到锁,就继续执行(有选择执行)如下:

std::chrono::milliseconds timeout(100);
if (my_mymutex.try_lock_for(timeout)) {
  //......拿到锁返回ture
} else {
  std::chrono::milliseconds sleeptime(100);
  std::this_thread::sleep_for(sleeptime);
}

try_lock_until():
参数是一个未来的时间点,在这个未来的时间没到的时间内,如果拿到了锁头,流程就走下来,如果时间到了没拿到锁,流程也可以走下来。

std::chrono::milliseconds timeout(100);
if (my_mymutex.try_lock_until(chrono::steady_clock::now() + timeout)) {
    //......拿到锁返回ture
} else {
    std::chrono::milliseconds sleeptime(100);
    std::this_thread::sleep_for(sleeptime);
}

两者的区别就是一个参数是时间段,一个参数是时间点

  • 5.2 std::recursive_timed_mutex:是待超时的递归独占互斥锁
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-05-09 12:22:52  更:2022-05-09 12:24:45 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 22:53:45-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码