C++11深入学习知识点整理(二)

异步、并发、进程、线程、协程相关知识点整理。


[名词解释]

<https://www.jianshu.com/p/6dde7f92951e<<协程>>

<https://www.fanhaobai.com/2017/11/synchronised-asynchronized-coroutine.html<异步、并发、协程原理>>

<https://blog.csdn.net/eunice_fan1207/article/details/91894192<C++协程>>

<http://www.langzi.fun/%E5%8D%8F%E7%A8%8B%E4%B8%8E%E5%BC%82%E6%AD%A5IO.html<协程与异步IO>>

<https://blog.csdn.net/kobejayandy/article/details/11856735<协程和异步>>

<https://blog.csdn.net/weixin_43705457/article/details/106924435<有栈协程与无栈协程>>

<https://blog.csdn.net/qq910894904/article/details/41699541<浅谈我对协程的理解>>

<https://blog.csdn.net/cbmljs/article/details/86574353<事件驱动的详解>>

<https://blog.csdn.net/lihao21/article/details/51620374<图解UNIX的I/O模型>>

 

$进程、线程、协程的对比

- 协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程与进程、线程相比并不是一个维度的概念。

- 一个进程可以包含多个线程,一个线程可以包含多个协程。

- 一个线程内的多个协程虽然可以切换,但是多个协程是串行执行的,只能在一个线程内运行,没法利用CPU多核能力。

- 协程与进程一样,切换是存在上下文切换问题的。

$硬件上下文

尽管每个进程可以拥有属于自己的地址空间,但是所有进程必须共享CPU寄存器,因此,在恢复一个进程的执行前,内核必须确保每个寄存器装入了挂起进程时的值。进程恢复执行前必须装入寄存器的那一组数据称为硬件上下文。

$上下文切换

- 进程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户是无感知的。进程的切换内容包括页全局目录、内核栈、硬件上下文,切换内容保存在内存中。进程切换过程是由“用户态到内核态到用户态”的方式,切换效率低。

- 线程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户无感知。线程的切换内容包括内核栈和硬件上下文。线程切换内容保存在内核栈中。线程切换过程是由“用户态到内核态到用户态”, 切换效率中等。

- 协程的切换者是用户(编程者或应用程序),切换时机是用户自己的程序所决定的。协程的切换内容是硬件上下文,切换内存保存在用户自己的变量(用户栈或堆)中。协程的切换过程只有用户态,即没有陷入内核态,因此切换效率高。

---

$进程(PROCESS)

- 进程是资源分配的最小单位

- 进程间不共享内存,每个进程拥有自己独立的内存

- 进程间可以通过信号、信号量、共享内存、管道、队列等来通信

- 新开进程开销大,并且 CPU 切换进程成本也大

- 进程由操作系统调度

- 多进程方式比多线程更加稳定

$线程(THREAD)

- 线程是程序执行流的最小单位

- 线程是来自于进程的,一个进程下面可以开多个线程

- 每个线程都有自己一个栈,不共享栈,但多个线程能共享同一个属于进程的堆

- 线程因为是在同一个进程内的,可以共享内存

- 线程也是由操作系统调度,线程是 CPU 调度的最小单位

- 新开线程开销小于进程,CPU 在切换线程成本也小于进程

- 某个线程发生致命错误会导致整个进程崩溃

- 线程间读写变量存在锁的问题处理起来相对麻烦

$协程(COROUTINE)

- 对于操作系统来说只有进程和线程,协程的控制由应用程序显式调度,非抢占式的

- 协程的执行最终靠的还是线程,应用程序来调度协程选择合适的线程来获取执行权

- 切换非常快,成本低。一般占用栈大小远小于线程(协程 KB 级别,线程 MB 级别),所以可以开更多的协程

- 协程比线程更轻量级

#应用程序和内核

内核具有最高权限,可以访问受保护的内存空间,可以访问底层的硬件设备。而这些是应用程序所不具备的,但应用程序可以通过调用内核提供的接口来间接访问或操作。所谓的常见的 IO 模型就是基于应用程序和内核之间的交互所提出来的。以一次网络 IO 请求过程中的 read 操作为例,请求数据会先拷贝到系统内核的缓冲区(内核空间),再从操作系统的内核缓冲区拷贝到应用程序的地址空间(用户空间)。而从内核空间将数据拷贝到用户空间过程中,就会经历两个阶段:

等待数据准备

拷贝数据

也正因为有了这两个阶段,才提出了各种网络 I/O 模型。

前四种I/O模型的主要区别在于第一个阶段,它们的第二个阶段是一样的:在数据从内核复制到应用进程的缓冲区期间,进程会被阻塞于recvfrom系统调用。

而异步I/O模型则是整个操作完成内核才通知应用进程。

#同步和异步

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)。

所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由*调用者*主动等待这个*调用*的结果。

而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。

#阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

#并发和并行

并发(concurrency):逻辑上具备同时处理多个任务的能力。

并行(parallesim):物理上在同一时刻执行多个并发任务,依赖多核处理器等物理设备。

多线程或多进程是并行的基本条件,但单线程也可用协程做到并发。通常情况下,用多进程来实现分布式和负载平衡,减轻单进程垃圾回收压力;用多线程抢夺更多的处理器资源;用协程来提高处理器时间片利用率。现代系统中,多核 CPU 可以同时运行多个不同的进程或者线程。所以并发程序可以是并行的,也可以不是。

#协同式和抢占式

实际上,调度方式并无高下,完全取决于应用场景。抢占式系统允许操作系统剥夺进程执行权限,抢占控制流,因而天然适合服务器和图形操作系统,因为调度器可以优先保证对用户交互和网络事件的快速响应。当年 Windows 95 刚刚推出的时候,抢占式多任务就被作为一大买点大加宣传。协同式调度则等到进程时间片用完或系统调用时转移执行权限,因此适合实时或分时等等对运行时间有保障的系统。

#协程与并发

协程要利用多核优势就需要比如通过调度器来实现多协程在多线程上运行,这时也就具有了并行的特性。如果多协程运行在单线程或单进程上也就只能说具有并发特性。

[并发编程(多线程)]

多线程意味着单个线程执行是顺序的。多个线程之间,如果没有同步的话,执行顺序是任意的,可先可后可同时。

多线程存在内存可见性的问题(写在读之前;正常刷新到内存)。因为硬件优化,重排序和存储缓存的存在,导致多线程执行的可能性更多。

#C++11线程中的几种锁(互斥锁、条件锁(条件变量)、自旋锁、读写锁、递归锁)

<https://blog.csdn.net/xy_cpp/article/details/81910513<C++11线程中的几种锁>>

---

<https://blog.csdn.net/u013749068/article/details/82055331<自旋锁>>

<https://www.jianshu.com/p/9d3660ad4358?utm_source=oschina-app<自旋锁到底是什么>>

<https://blog.csdn.net/u012834750/article/details/69398216<多线程(十二)锁的种类及辨析>>

 

$临界资源和临界区

<https://blog.csdn.net/Marmara01/article/details/84638258<临界资源和临界区>>

临界资源:

       各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有打印机、磁带机等,软件有消息缓冲队列、变量、数组、缓冲区等。 诸进程间应采取互斥方式,实现对这种资源的共享。

 

临界区:

       每个进程中访问临界资源的那段代码称为临界区。显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。为此,每个进程在进入临界区之前,应先对欲访问的临界资源进行检查,看它是否正被访问。如果此刻该临界资源未被访问,进程便可进入临界区对该资源进行访问,并设置它正被访问的标志;如果此刻该临界资源正被某进程访问,则本进程不能进入临界区。

 

$互斥锁和临界区(windows)

<https://blog.csdn.net/xdrt81y/article/details/17005235<C++多线程之使用Mutex和Critical_Section>>

---///---

$条件变量(std::condition_variable和std::condition_variable_any)

<https://www.cnblogs.com/5iedu/p/11894925.html<线程同步(std::condition_variable)>>

<https://blog.csdn.net/a13602955218/article/details/106031020<wait,notify_one,notify_all>>

<https://blog.csdn.net/qq_41453285/article/details/105605310<条件变量:condition_variable、condition_variable_any>>

<https://www.cnblogs.com/haippy/p/3252041.html<std::condition_variable 详解>>

<https://blog.csdn.net/imred/article/details/103811929<条件变量为什么要和互斥量一起使用?>>

 

m_lock:获取互斥量(锁);

m_cond.wait:释放持有的锁,加入到等待队列中;

notify_one:唤醒等待队列中的第一个线程;

notify_all:唤醒等待队列中的所有线程;

 

线程被唤醒后,重新竞争互斥量(锁),得到锁的线程从wait中返回。直到离开m_lock的作用于或重新调用wait,锁才被释放。

 

需要注意的是,这里有两条队列,一条是获取互斥量的队列,另一条是条件变量中的等待队列

 

$自旋锁(spin lock)与互斥量(mutex)使用分析

<https://www.zhihu.com/question/38857029<自旋锁和使线程休眠的非自旋锁各有什么适用场景?>>

<https://www.cnblogs.com/zifeiye/p/8194949.html<C++ 并行编程之memory_order>>

<https://www.zhihu.com/question/24301047<如何理解 C++11 的六种 memory order?>>

 

自旋锁是一种busy-waiting的锁;互斥锁是一种sleep-waiting的锁。

自旋锁(spin lock)与互斥量(mutex)的比较自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

互斥量是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。

 

两种锁适用于不同场景:

如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。

如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥量。

如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等到运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。

如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥量的开销较大。

 

$std::shared_mutex读写锁(c++17)

<https://blog.csdn.net/gongjianbo1992/article/details/100061344<C++ std::shared_mutex读写锁>>

<https://blog.csdn.net/zxc024000/article/details/88814461<c++读写锁实现>>

 

shared_mutex 拥有两个访问级别:

共享:多个线程能共享同一互斥的所有权(如配合shared_lock);

独占:仅有一个线程能占有互斥(如配合lock_guard、unique_lock)。

shared_mutex 通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。

 

如果任意一个线程拥有一个共享锁,试图获取独占锁的线程会被阻塞,直到其他线程全都撤回它们的锁。同样的,如果一个线程具有独占锁,其他线程都不能获取共享锁或独占锁,直到第一个线程撤回它的锁。

 

$递归锁(std::recursive_mutex)(一般不使用递归锁)

<https://blog.csdn.net/llb_1203/article/details/107948953<recursive_mutex>>

<https://blog.csdn.net/mcc12356/article/details/106947074<递归锁>>

<https://blog.csdn.net/m18718300471/article/details/79927948<递归锁recursive_mutex的原理以及使用>>

一般的互斥量对于同一个线程只能同时加锁一次(调用lock),如果连续调用两次以上便会出现死锁。你可能会问,为什么会连续调用两次呢,但设想这样一个场景,如果在一个函数内给互斥量上了锁还没有解开,然后又调用了另一个函数,这个被调用的函数也去给互斥量上锁,这种情境下就会出现死锁。

 

递归锁用起来固然简单,但往往会隐藏某些代码问题。比如调用函数和被调用函数以为自己拿到了锁,都在修改同一个对象,这时就很容易出现问题。(同一个线程不会出现问题,不同线程的话会出现死锁)

 

 

#原子操作语义(std::atomic)和内存模型

<https://blog.csdn.net/liuxuejiang158blog/article/details/17413149<std::atomic原子操作>>

<https://blog.csdn.net/weixin_30664051/article/details/95261415<原子操作atomic>>

<https://www.codedump.info/post/20191214-cxx11-memory-model-1/<C++11中的内存模型上篇 - 内存模型基础>>

<https://www.codedump.info/post/20191214-cxx11-memory-model-2/<C++11中的内存模型下篇 - C++11支持的几种内存模型>>

<https://www.cnblogs.com/catch/p/4158495.html<再说 c++11 内存模型>>

<https://blog.csdn.net/gqtcgq/article/details/52330065?utm_source=blogxgwz7<互斥锁和内存可见性<>

<https://www.cnblogs.com/5iedu/p/11964414.html<第31课 std::atomic原子变量>>/<https://zhuanlan.zhihu.com/p/103425537?utm_source=wechat_timeline<C++内存模型小记>>

<https://zhuanlan.zhihu.com/p/129268305<并发编程基础 - C++原子操作语义和内存模型>>

 

$atomic types

C++ 的标准原子类型在 <atomic> 头文件里。这些 atomic types 的所有操作都是原子的,C++标准定义下也只有这些类型的操作是原子的(显然你可以用 mutex 之类的同步原语自己实现原子性)。原子指的是无法观察到操作执行了一半的不完整状态。举个例子,int x=0; 两个线程分别 x++ 1000次,最终 x 大概率是不等于 2000 的,x++ 其实会拆成读 x,然后算 x+1,再把结果写会 x 几个操作,可能有两个线程读到同一个 x 值,然后各自算 x+1,最后写会 x+1,这样就少加了一次,因为读到了不完整状态。而使用 std::atomic<int> x = 0;,使用 fetch_add 就能解决这个问题,因为对 int 自增是非原子的,而 std::atomic<int>::fetch_add 是原子的。

 

注意,atomic types 的原子性是从语言标准层面说的,具体实现可能是基于 CPU 提供的原子操作指令,也可能是基于锁的。通过 is_lock_free() 成员函数可以判断这个原子类型是否是 lock-free 的。std::atomic_flag 不提供 is_lock_free(),这个类型就是个布尔类型(注意与 std::atomic_bool 区分),标准要求这个类型的操作必须实现成 lock-free 的。当然了,大多数平台的内建类型对应的原子类型是 lock-free 的,比如 atomic<int>, atomic<void*> (这不是强制要求)。

 

$sequenced-before

sequenced-before用于表示单线程之间,两个操作上的先后顺序,这个顺序是非对称、可以进行传递的关系。

 

它不仅仅表示两个操作之间的先后顺序,还表示了操作结果之间的可见性关系。两个操作A和操作B,如果有A sequenced-before B,除了表示操作A的顺序在B之前,还表示了操作A的结果操作B可见。

 

$happens-before

与sequenced-before不同的是,happens-before关系表示的不同线程之间的操作先后顺序,同样的也是非对称、可传递的关系。

 

如果A happens-before B,则A的内存状态将在B操作执行之前就可见。在上一篇文章中,某些情况下一个写操作只是简单的写入内存就返回了,其他核心上的操作不一定能马上见到操作的结果,这样的关系是不满足happens-before的。

 

$synchronizes-with

synchronizes-with关系强调的是变量被修改之后的传播关系(propagate),即如果一个线程修改某变量的之后的结果能被其它线程可见,那么就是满足synchronizes-with关系的。

 

显然,满足synchronizes-with关系的操作一定满足happens-before关系了。

 

$内存屏障和重排序和内存可见性

通过使用内存屏障,可以保证所有的重排序都已完成,所有挂起的写操作都已执行。因此其他线程的数据一致性得以保证,从而保证了良好的内存可见性。而且:互斥锁的实现中,使用了我们所需要的内存屏障。

 

互斥锁通常被介绍为是一种保证原子访问的机制。但是实际上,锁不仅用于管理共享对象的访问,它还用于处理内存可见性的问题。某些情况下,atomic原子访问不成问题,但是内存可见性却至关重要。

 

C++11的6种memory order

内存顺序

作用

memory_order_relexed

只保证当前操作的原子性。

不考虑线程间的同步,其它线程可能读到旧值(因为不指定内存屏障,所以内存操作执行时可能是乱序的)

memory_order_acquire

①对读取(load)实施acquire语义。相当于插入一个内存读屏障,保证之后的读操作不会被重排到该操作之前

类似于mutex的lock操作,但不会阻塞。只是保证如果其它线程的release己经发生,则本线程其后对该原子变量的load操作一定会获取到该变量release之前发生的写入。因此,acuquire一般需要用循环,等待其他线程release的发生

memory_order_release

①对写入(store)实施release语义(类似于mutex的unlock操作)。保证之前的写操作不会被重排到该操作之后,同时确保该操作之前的store操作将数据写入cache或memory中,确保cache或memory是最新数据。(相当于插入一个写屏障)。

②该操作之前当前线程内的所有写操作,对于其他对这个原子变量进行acquire或assume的线程可见。(对于assume,写操作指对依赖变量的写操作)

③该操作本身是原子操作。

memory_order_consume

类似memory_order_acquire,但只对与这块内存有关的读写操作起作用。

memory_order_acq_rel

对读取和写入施加acquire-release语义,无法被重排(相当于同时插入读写两种内存屏障)。

②可以看见其他线程施加release语义的所有写入,同时自己release结束后所有写入对其他acquire线程可见。

memory_order_seq_cst

如果是读取就是acquire语义,写入就是release语义,读写就施加acquire-release语义

②同时会对所有使用此memory-order的原子操作建立一个全局顺序这样,所有线程都将看到同样一个的内存操作顺序。

#volatile

<https://blog.csdn.net/weixin_44363885/article/details/92838607<详解C/C++中volatile关键字>>

<https://www.zhihu.com/question/31459750<多线程编程中什么情况下需要加 volatile?>>

<https://www.cnblogs.com/viviancc/archive/2012/03/09/2388197.html<剖析为什么在多核多线程程序中要慎用volatile关键字?>>

 

C/C++多线程编程中不要使用volatile。

(注:这里的意思指的是指望volatile解决多线程竞争问题是有很大风险的,除非所用的环境系统不可靠才会为了保险加上volatile,或者是从极限效率考虑来实现很底层的接口。这要求编写者对程序逻辑走向很清楚才行,不然就会出错)

 

C++11标准中明确指出解决多线程的数据竞争问题应该使用原子操作或者互斥锁。

C和C++中的volatile并不是用来解决多线程竞争问题的,而是用来修饰一些因为程序不可控因素导致变化的变量,比如访问底层硬件设备的变量,以提醒编译器不要对该变量的访问擅自进行优化。

 

对访问共享数据的代码块加锁或者使用原子加内存屏障,已经足够保证数据访问的同步性,再加volatile完全是多此一举。

#高并发

<https://www.pianshen.com/article/95031456671/<多线程与高并发>>

#线程个数的选择

<https://www.jianshu.com/p/f30ee2346f9f<面试问我,创建多少个线程合适?我该怎么说>>

$对于CPU密集型程序:

CPU 核数(逻辑)+ 1 个线程数是比较好的

$对于 I/O 密集型程序:

最佳线程数 = (1/CPU利用率) = 1 + (I/O耗时/CPU耗时)

#线程进程协程的使用场景分析

<https://www.zhihu.com/question/283126971/answer/429518054<什么时候使用多线程?什么时候使用多进程?I/O密集型?CPU密集型?>>

<https://blog.csdn.net/qq910894904/article/details/41699541<浅谈我对协程的理解>>

 

从硬件发展来看,从最初的单核单CPU,到单核多CPU,多核多CPU,似乎已经到了极限了,但是单核CPU性能却还在不断提升。server端也在不断的发展变化。如果将程序分为IO密集型应用和CPU密集型应用,二者的server的发展如下:

IO密集型应用: 多进程->多线程->事件驱动->协程

CPU密集型应用:多进程-->多线程

 

如果说多进程对于多CPU,多线程对应多核CPU,那么事件驱动和协程则是在充分挖掘不断提高性能的单核CPU的潜力。

 

#C++11为什么引入多线程,而没有多进程?

<https://zhidao.baidu.com/question/362413704380280412.html<c++ 11 外什么引入多线程 而没有多进程>>

1:多线程的开发和维护比多进程要更复杂。

2:linux和window的进程概念差不多,但是线程概念差别很大。

3:进程完全是操作系统层面的,线程可以是语言层面的

4:跨平台的多线程程序移植一直是程序员最头疼的问题之一,C++的历史上有着众多的线程库。各自的接口和原语都不一样,所以定义一个语言层次上的标准的线程库接口是非常必要的。

5:多进程一个fork(VC:CreateProcess)就能搞定了,没什么太多接口需要定义,不需要专门为了进程去定义一个库。

#线程崩溃是否会造成进程崩溃?

<https://www.zhihu.com/question/22397613<线程崩溃是否会造成进程崩溃?>>

$会整个崩溃

当一个线程向非法地址读取或者写入,无法确认这个操作是否会影响同一进程中的其它线程,所以只能是整个进程一起崩溃。

#c++11实现多线程任务池

<https://www.jianshu.com/p/6e21f67a6ada<多线程任务池实现c++11>>

<https://www.cnblogs.com/5iedu/p/11894925.html<利用条件变量实现线程池>>

#sleep的三种方式(跨平台)

<https://blog.csdn.net/sinat_18811413/article/details/104767843<c++中实现sleep的三种方式(跨平台)>>

#C++11的std::thread

<https://www.cnblogs.com/mmc9527/p/10427924.html<C++11并发之std::thread>>

#C++11的join()和detach()

<https://blog.csdn.net/qq_36784975/article/details/87699113<C++11多线程join()和detach()的理解>>

join()函数是一个等待线程函数,主线程需等待子线程运行结束后才可以结束,如果打算等待对应线程,则需要细心挑选调用join()的位置

detach()函数是子线程的分离函数,当调用该函数后,线程就被分离到后台运行,主线程不需要等待该线程结束才结束

 

#C++11的std::future和std::promise和std::packaged_task和std::async

<https://www.cnblogs.com/wangshaowei/p/8875501.html<C++11之std::future和std::promise和std::std::packaged_task>>

std::future是一个非常有用也很有意思的东西,简单说std::future提供了一种访问异步操作结果的机制。从字面意思来理解,它表示未来,我觉得这个名字非常贴切,因为一个异步操作我们是不可能马上就获取操作结果的,只能在未来某个时候获取,但是我们可以以同步等待的方式来获取结果,可以通过查询future的状态(future_status)来获取异步操作的结果。future_status有三种状态:

 

deferred:异步操作还没开始

ready:异步操作已经完成

timeout:异步操作超时

 

获取future结果有三种方式:get、wait、wait_for,其中get等待异步操作结束并返回结果,wait只是等待异步操作完成,没有返回值,wait_for是超时等待返回结果。

#c++11的lock_guard和unique_lock

<https://blog.csdn.net/guotianqing/article/details/104002449<c++11中的lock_guard和unique_lock使用浅析>>

<https://www.cnblogs.com/xudong-bupt/p/9194394.html<C++ 并发编程,std::unique_lock与std::lock_guard区别示例>>

 

lock_guard是一个互斥量包装程序,它提供了一种方便的RAII(Resource acquisition is initialization )风格的机制来在作用域块的持续时间内拥有一个互斥量。

 

unique_lock是一个通用的互斥量锁定包装器,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。

#std::chrono和time_t

<https://www.cnblogs.com/qicosmos/p/3642712.html<c++11中的日期和时间库>>

<https://www.cnblogs.com/yukaizhao/archive/2011/04/29/cpp_time_system_time.html<C++时间标准库时间time和系统时间的使用>>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值