c++多线程
当thread不持有线程的时候,get_id返回0x0.
thread.joinable只要thread持有线程,那么就是true,无论线程是否结束
如果在joinable的情况下调用析构,那么就会调用std::terminate终结程序。
实际上thread只是一个包装,真正的线程是里面的线程句柄,native_handle,也就是要用pthread库来处理的对象,可以通过native_handle()获取句柄。
里面的这个pthread_t的类型中会存储线程的数据啊,比如调用对象,参数之类的。
std::thread::hardware_concurrency()返回硬件支持的并行度
thread本身不是atomic的,不能同时有多个线程同时调用它的join,一方面可能导致二次调用,一方面可能造成数据竞争
但detach之后,thread这个包装器也就不再管理这个线程句柄了,线程句柄的资源会在它自己结束的时候自己释放。并且在detach之后它就空,get_id为0,joinable为false。而如果不detach,那么即便线程结束了,它的资源也不会被释放,直到.join被调用。
thread的构造函数第一个参数时fun,后面就是它对应的args,当且仅当匹配的时候不会报错。也因此在创建的时候它就开始运行了。
正常的thread在析构的时候如果还在运行就会调用std::terminate,而jthread则会在析构的时候先调用stop_request再调用join,结束后才停止。这里之所以要增加一个stop_request是为了告诉那个线程该停止了,因此你该做些操作别再运行了,而如果没有的话,可能thread就一直搁这运行,然后我想要join,就永远卡在join这里了。因此,就需要一个共享变量,让我能够在这个线程就告诉jthread该停止了。因此jthread还维护了一个std::stop_source。
这里要提到三个东西,一个是停止状态,也就是真正的那个用来表示当前是否被要求停止的atomic
而为了让jthread线程运行的函数能够使用这个停止状态,线程运行的函数的参数的第一个可以是一个std::stop_token,而jthread具体去运行的时候也是fun(stop_source.get_stop_token(),args…),因此函数就可以去使用这个stop_token了。通过stop_requested来判断是否呗要求停止并做出相关操作。
这些停止操作被称为协作式取消。而除了上述的停止源和停止令牌以外,还有停止回调,就是停止后要被调用的函数,在请求request_stop的线程上调用。但要求它的生命周期一定要到request_stop之后结束。否则就不会调用。
用get_id可以获取当前调用get_id的线程的id,即当前正在运行的线程。
yield可以让操作系统对于线程重新调度,暂时不要调度自己,但具体行为依赖于操作系统,所以不常用。感觉不如this_thread.sleep_for/until
伪共享:一般缓存都是按一行去管理和处理的,例如一起缓存一行啊之类的。但是,既然是缓存,就有可能脏,那么就需要与其他数据去同步,而如果此时也有另一线程同样在处理这一缓存行,就烂完了。因为,线程运行在不同核心上,每个核心都有自己的缓存,而如果发现有竞争,他们就会竞争这一缓存行的所有权,并影响到更加靠后的更加慢的缓存。然后就导致速度大幅下降。但有的时候,他们实际上竞争的不是同一个数据,而只是恰好储存在统一缓存行的不同的数据。而这就是伪共享,也就是这两个变量实际上都不是同时被两个线程共享的。因此我们可以在写的时候就让他们间距超过一个缓存行,让他们处于不同缓存行上就可以了。而如果发现有一些变量确实是真的被共享的,也可以将他们放在同一个缓存行上。
std::hardware_destructive_interference_size, std::hardware_constructive_interference_size
这两个函数就是用来获取避免假数据共享的最小偏移和鼓励真共享的最大连续内存大小。
内存顺序。由于多核和缓存的存在,实际上读数据的时候并不是统一的读的,也不知道会从哪个随机的可以读的地方读,就导致当操作同一元素的时候就会导致看到的情况和真实情况不一致的问题。因此需要引入内存顺序规定顺序以简化。
有多种原因会导致,编译器重排,cpu重排,多线程,缓存等等都会。但是这在语言层面上并不重要,语言层面讲这些统一称为可见性。即,我所进行的更改你看不看的到。
现在主流的总共有3种,relaxed,即什么都不保证。acquire和release,acquire的效果是该load之后的所有读写不能放到本操作之前,并保证此原子变量在其他线程所有的释放操作在此之后必可见。release就是保证之前的读写不能重排到本操作后,并保证本操作之前的所有操作对加载本原子的可见。acl_rel就是同时有这两点,又读又写。最后是seq_cst,也就是*序列一致定序。*前面的获取释放已经很强了,但是呢,它只保证关于这个原子数据的操作,只保证这一对读写之间的可见性。但如果涉及到多个原子变量,并有多个线程进行不同的load,它的可见性就不一定在各个线程中保持相同了,可能第一个先acq A,然后B,第二个先acq B,然后A,那么对于他们来说,对于A和B是什么时候写的就不是同步的了,而seq_cst则能保证他们一定会被排成一个序,对于所有线程都一样可见,但性能较低。
原子变量只保证,它的读写是原子的,至于原子之间的,多线程读写,原子与非原子的这些全部由内存顺序决定,当使用原子操作而不带参数的时候默认是序列一致定序。
atomic,所有操作均原子,不存在数据竞争。除去ptr和shared/weak_ptr之外,只有能够平凡拷贝且可移动的能够用atomic。
atomic之间不能拷贝移动,但能将非atomic复制过来。
但是它的原子实现有可能是使用锁,也有可能是使用指令集提供的原子内存操作。一般来说标准的大小合适(比如小于8字节)的内存对齐的类型就会使用原子内存操作,也就是无锁的,非常高效。但如果是使用锁的,可能比显式使用mutex的性能还烂。而这在不同的平台上是不同的。因此c++也提供is_lock_free函数来查询。
一般总是有几个类型是免锁的,但是不知道是哪个,可以用is_always_lock_free或者atomic_signed_lock_free和atomic_unsigned_lock_free来判断或者获取。
atomic的=等价于store函数。
取值load。
交换exchange。
compare_exchange_weak/strong ,比较,如果相等就交换为desired,否则让expected为不相等的值(这是为了保证原子性)并返回false。就是CAS在c++层的封装。
weak和strong的区别是strong会要求独占,所以会性能较烂。而weak不要求,但可能会出现假性失败,也就是即便是相等,也可能返回false。
当我们一定希望它最终能出现true返回的时候,也就是一定要替换成功的时候,比如在实现无锁的时候的赋值的时候,我需要同时更改head和head.next的时候,并且希望它是原子的,就需要用weak来while。而如果只是想判断一下它是多少,那么就只会调用一次,那么就使用strong,因为weak完全不知道什么时候是真正的失败,并且相较于load了之后再==,这样是原子的。
wait(old),如果与old相等就开始阻塞,直到被唤醒。
notify_one和notify_all
fetch_add,fetch_sub,fetch_max(用自己和给的中大的替换自己),fetch_min,还有一些逻辑运算。
不包括其他的四则运算。
atomic_ref对于一个非原子已存在对象提供一个原子视图的引用,但如果一个对象呗atomic_ref引用的状态下,就只能通过ref来访问而不能去使用对象本身。
atomic_flag实际上类似于atomic
永远都不应该在用户空间自己实现自旋锁,因为,用户空间即便是在锁的状态下也是会被操作系统调度的。只有内核空间能够做到完全自旋,而不被抢占。
atomic_thread_fence不跟具体的原子绑定,它所处理的是所有非原子和relaxed原子操作的内存序。它寻找它之后的release或者它之前的acquire作为一个间接节点,让自己之前或者之后的操作先于或者后于这个间接节点对应操作前或后的所有操作。
但按照网上的说法它和原子操作的内存序有点区别。原子操作的release不允许之前的读写排到它之后,但允许后面的排到之前,而fence不允许,反之依然。
但实际上一般不太会需要使用atomic_thread_fence,用atomic和mutex就够了。
总结而言,多线程这个东西,编写起来主要有两个问题。一个是每个线程都是按顺序执行的情况下的,如何保证其中不同线程之间的执行顺序的同步。另一个是,无论是cache还是编译器,cpu导致的最终看起来好像产生了指令重排一样(无论是单个线程的指令重排还是不同线程之间的重排不同,即内存操作序列不同),这些可能会导致问题。而mutex,atomic和memory_order也有一部分是为了解决这一问题。正是因为多线程,因此就导致最终运行的程序无法知道自己具体的情况,就不能好好的完美无缺的最快的运行,因此要我们去指引顺序。一般来说就是谁必须在谁之前执行完毕,也就是release和acquire,release是前面的不能往后移动,而acquire是后面的不能往前移动,barriar则是双向的。
mutex,不应该在被持有的状态下销毁或终止或析构
lock:如果没有人持有,就acquire,否则阻塞,不能本线程在持有时调用
try_lock:如果没有人持有,就acquire,并返回true,否则false
unlock:必须是在本线程持有lock的时候
timed_mutex,多提供两个函数,try_lock_for/until,让其在一段时间内尝试获取,没获取到就false
recursive_mutex,能够被同一线程多次锁定,但也要同样次数的unlock
recursive_timed_mutex
shared_mutex,共享锁,对于三个基本函数额外提供shared版本
shared_timed_mutex
lock_guard,RAII,构造时lock,析构时unlock,提供作用域内的关键段。
scoped_lock,类似于lock_guard,但可以同时对多个mutex acquire,并且可以避免死锁。
实际上是std::lock的RAII版本,做到避免死锁的方法是如果不能一次性全锁定成功,就全部回退并阻塞。
std::try_lock,也是锁多个,检查一个锁一个,碰到第一个不能锁的就开始反过来一个一个解锁并返回false。
std::call_once(std::once_flag(一个专用的类型,保证对于同一个once_flag的所有call_once只会被call一次),fun, args…)如果返回异常则立即抛出且这次不算。
接着是最常用的,unique_lock,只能移动,若只传入mutex则类似guard,构造锁定,如果同时还传入策略,那么defer则不锁,try尝试,adopt假设已经上锁,还可以传入时间和时间点构造timed。析构时解锁,但是会判断自己是否关联并持有mutex。
除了3个函数和timed函数,还有swap交换看,release不关联但不处理锁的状态,operator bool和owns_lock来判断是否lock状态。
实际上是lock的RAII。
因此还有shared_lock对应shared_lock的RAII。
条件变量可能会被虚假唤醒,虚假唤醒就是被唤醒的时候发现条件还没有满足。有三种说法,一种说是广播了,因此,可能被唤醒的时候已经有别的线程事先抢占进行了更改,一种是在就绪和被调度期间被其他线程导致条件失败,另一种说是为了效率系统层面上就允许即便条件没有满足wait也可能返回。
当cv开始wait的时候unlock mutex,即允许其他线程持有这个mutex做些事情比如改变条件变量,而其他线程则必须要在持有锁的情况下更改条件变量相关的变量,无论是否原子,并在结束后unlock并notify,wait就会检查条件是否满足,如果满足就获取锁并不再wait。
notify_one执行完的瞬间我们可以假设所有wait的其中一个已经获得了锁。
conditional_variable_any,不止可以用于unique_lock,也可以是shared_lock
当退出线程时如果还持有锁,那么就是未定义的。因此可以notify_all_at_thread_exit来保证到线程结束的时候,都持有着锁的状态,然后销毁thread_local,unlock并notify_all。
信号量提供一个更简单的条件变量,只有数字增减,性能比条件变量高。通过acquire来使数字-1,如果为0了,那么就阻塞,release来增加数字。无需notify。counting_semaphore<最大值>
屏障,一次性屏障latch,构造时设定初始值,count_down-1单不阻塞,wait阻塞直到为0,try_wait判断是否为0不阻塞,arrive_and_wait,count_down+wait。
barriar,可重用屏障。构造时设定初始值和on_completion要执行的函数,当为0时运行completion函数并重置计数。
上述都是自己要手动开一个线程去跑任务,但有的时候我只是希望它新开一个线程自己跑然后最后给我结果就行了。因此就出现了promise和future。
promise和future是一对相关的对象,一般先创建promise,然后从中获取对应的future。
将promise传给线程函数,通过set_value/exception(_at_thread_exit)就可以设定异步的返回值。而future可以通过get进行获取,如果还没有设定,就会阻塞。
packaged_task就是把函数直接打包进promise里了,return就是set_value,而exception也会自动设置。
future本身是只可以移动的,不能共享,而shared_future就是可以复制的,可以共享。
async就是将启动函数的这一步也直接打包了,直接返回一个future。
并可以在构造时设定启动参数,async策略就是立即异步执行,defer就是get的时候再执行。
future只能get一次,promise也只能set一次,当出现第二次的时候就会报错future_already_retrieved和promise_already_satisfied,而如果是空构造的future,去调用get就会返回no_state,promise也可以显式表示自己无法完成promise,抛出broken_promise。当抛出错误的时候,会存储在future中,但get的时候会被rethrow。