《C++ Concurrency in Action - SECOND EDITION》笔记
第一章 你好,C++的并发世界
进程并发的优点:
- 依赖于操作系统提供的通信手段,可以有效地保护进程不被其他进程访问和修改,操作系统提供了保护操作和更高级别的通信机制。
- 可以实现不同主机之间的并发(网络通信)
进程并发的缺点:
- 进程占用资源更多,启动更慢,通信手段复杂或者低效。
- C++没有提供对多进程的支持,依赖操作系统,不方便跨平台
线程并发的优点:
- 内存共享,方便地进行通信
- 资源消耗小,启动更快
- c++支持,所以可以编写跨平台代码
线程并发的缺点:
- 线程的共享管理更复杂,需要良好的维护
- 无法跨主机
1 |
|
注意这个语法可能会被解析成为函数声明,因此可以用my_thread{backgroud_task())}等。被称为最令人头痛的语法解析
注意一旦声明了一个线程,就必须要在thread被销毁之前决定是join还是detach。否则的话当thread被销毁的时候会调用terminal 杀死进程。注意杀死的不是什么别的东西,杀死的是进程本身。所以主线程和子线程都死了!
而且注意到一个很奇怪的事实就是,如果main函数退出的话,所有线程也被杀死了。看起来main函数退出是进程退出,资源回收(进程是资源分配的单位,线程也是资源),所以所有的线程也要退出。
join 加入,等到线程完成之后再退出。要注意如果主线程异常的话,join会被跳过。
下面的一些代码展示如何避免这个问题,就是捕捉异常之后要及时地决定线程join或者detach。
1 |
|
根据异常之后会自动进行析构函数销毁变量,可以重定义thread,让他在析构的时候首先join。
1 |
|
detach,分离,一个thread对象调用detach,表示把线程和这个对象分离开,主要是让线程在后台运行。thread被销毁不会影响到他了。注意线程如果引用了临时变量,那么当主线程销毁的时候,该临时变量也可能变得未定义了。
1 |
|
注意看上面这段代码,some_local_state是局部变量,并且直接被my_func使用。当采用detach策略,那么当oops运行完毕的时候,这个变量就会销毁,从而my_func访问了未定义变量。
【引用的底层实现机制,为什么std::ref()能够提供引用,为什么thread传参需要复制而非引用(可能是无视函数的需求)】
1 |
|
注意看这段文字的初始化方法,第一个参数实际上不是对象的函数,而是类的函数,而第二个参数提供了对象的起始地址,这个方法应该等价于t(my_x.do_lengthy_work))
线程可以移动,但不能复制,所以同一时间线程只能和一个thread关联,但可以在不同thread之间移动。
1 |
|
看上面这段例子,t2初始化的对象是t1,t1作为thread可移动但不能复制。所以如果直接用=则是复制操作会崩溃。而3位置是可以的,因为右边是临时对象,移动操作将会隐式的调用。就是移动赋值。
t3可以直接赋值,但是t1不行,因为t1有线程,所以直接调用terminal终止了进程。不能通过赋值的方式丢弃进程,只能detach,或者join等待线程完成,然后t1才能空下来,这个时候就可以赋值了。
【std::thread
对象的容器,如果这个容器是移动敏感的(比如,标准中的 std::vector<>
),那么移动操作同样适用于这些容器。】
标识符是std::thread::id类型,注意不同于int,尽管cout可以输出int。可以拷贝和对比,因为可以复用。
第三章
当其中一个成员函数返回的是保护数据的指针或引用时,会破坏数据。具有访问能力的指针或引用可以访问(并可能修改)被保护的数据,而不会被互斥锁限制。这就需要对接口有相当谨慎的设计,要确保互斥量能锁住数据的访问,并且不留后门。
接口之间可能存在竞争的关系,所谓接口就是开放给外部访问的数据?
比如,一个stack,size() empty 的返回结果是没问题的,但是是不可靠的,因为可能存在有其他竞争在其返回后访问stack。
一些重要的类含义记录:
lock_guard:这个类就是对某个互斥量的上锁, 同时在该对象被销毁的时候解锁。
unique_lock:这个类就是lock_guard的加强版,可以加第二个参数,要不要上锁。因为可以调用lock()函数统一上锁。这个函数需要参数都有lock() unlock() try_lock()三个参数。
shared_ptr:就是对某块堆上的数据进行智能的监控,当引用为0的时候自动销毁。比如说
1 |
|
对于这样的一句话,根据下图所示,首先调用new,生成了widget,然后shared_ptr会生成控制块也就是中间的,包括指针,被引用的次数等。
但是shared_ptrs都是强引用的,那么会有循环引用的问题:
比如说这个图中,当我退出程序了,那么Sp1和Sp2都被销毁了,这没啥问题,但是问题是他们指向的对象不会被销毁,因为他们互相引用。需要用weak_ptr在节点内部的指针。make_shared:这个是专门用来制作shared_ptr的,但是不同于上文中shared_ptr p = new int(),这里会发生两次alloc。通过make_shared可以将其减少为1次。
比如说make_shared (move(0))这样,会在分配内存的时候,将控制块和数据内容分配在一起。
所以实质上所谓的shared_ptr不过是个伪指针罢了。make_shared的好处在于,减少一次alloc开销,而这对性能提升是很大的,其次,占的内存更小,因为减少了大小跟踪的分配器开销,还有增强了数据的局部性,这两个块很可能是在同一个页面或者同一缓冲行。但是带来的问题就在于,由于这两个块分配在了一起,所以必须要等到weak_ptr被释放之后,那一整个块才能被释放(因为make_shared生成的是一整个块)。但是如果是两次分配的话,强指针为0的时候,对应的数据块就被释放了,等到弱指针释放之后,控制块被释放。
wait函数详解
这个函数支持一个锁和一个函数为参数。首先会调用这个函数,如果是true,那么就继续执行,如果是false,那么会释放前面的锁,注意,这个锁在传入的时候应该是锁上的。释放之后,进入睡眠状态(为什么要释放呢?因为往往是因为没有东西可以消费,所以阻塞在这里,要释放锁让其他线程从事生产)等到notice_one的时候,再唤醒,然后加锁,然后调用函数,重复上述过程。