Cpp Concurrency in Action笔记
Chapter 1: Hello, world of concurrency in C++!
Chapter 2: Managing threads
Chapter 3: Sharing data between threads
Mutex
虽然std::mutex提供了
lock/try_lock/unlock,但是在实际使用时还是尽可能使用std::lock_guard,利用RAII来完成加锁和解锁。std::lock_guard在C++ 17还有一个升级版本叫std::scoped_lock。竞态条件完全不是直接上锁就能避免的,还需遵从如下的几个原则。也就是说,必须严格把对于被mutex保护的内存的访问局限在同于个critical section,一点也不能泄露出去,因此在接口的设计上必须特别小心。
1
Don’t pass pointers and references to protected data outside the scope of the lock, whether by returning them from a function, storing them in externally visible memory, or passing them as arguments to user-supplied functions.
线程安全的Stack
以std::stack提供的功能为例,它提供了独立的empty/top/pop接口。这样做在单线程环境下是没有问题的,但是到了多线程环境下,任何的empty和top,以及top和pop之间都可能会有其他的栈操作。比如线程AB都先使用top得到栈顶的值,然后分别pop,结果就是栈顶下面的值在被读取之前就被pop了,显然不行。书中也提到了,这种设计的出发点是防止拷贝时因为内存不足抛出exception导致栈顶的值被丢弃,但是它在多线程环境下反而成了阻碍。
为了让stack能够在并发环境下工作,首先要对这三个接口进行一定程度上的合并。如果允许top和pop在栈空时抛出异常,那么empty基本就可以省略了。另外top和pop必须进行合并,为了能够正常出栈,需要把top删除,仅保留pop。所以,一个线程安全的栈仅具有pop和一个empty接口,保留empty接口是为了方便使用。
pop接口的设计也值得斟酌。避免bad_alloc导致操作不完整的最简单方式就是要求调用者预先在外界做好分配并传入引用,但是这种方法的局限性很大,比如某些情况下调用者无从得知要预留多大的空间。如果栈中元素类型有noexcept的拷贝或移动构造的话,说明bad_alloc根本不会出现,方法可以放心地返回一个值。另外容易看出,如果能在出栈之前就预留空间,这样即便有bad_alloc也不会造成操作不完整,因此还可以在pop内部分配一块动态内存用于缓存栈顶的值,然后将指针作为返回值。
书中给出的设计提供了传引用和返回指针两种接口,pop在栈为空时会抛出异常。

