What does it mean to design for concurrency
设计一个支持并发的数据结构意味着多个线程会通过调用相同或者不同的接口来访问数据结构,每一个线程都会看到一个自洽的对象。没有数据丢失或者损坏,所有不变量都会保持,同时不会有数据竞争。这样的数据结构称为线程安全(thread-safe
)。多个线程可以并发的使用一类操作,但是另外一些操作可能是排他的。或者是多个线程必须执行不同的操作而不能执行同一个操作。这些都是安全的,意义不同而已。
真正的并发要提供并发机会。互斥只能有一个线程访问,显式地阻止了真正的并发访问被保护的数据。
这本质上是串行化(serialization
)。我们必须认真思考,使之能真正并行访问。原则是保护的范围越小,越少的操作会被串行化,潜在的并发可能性越大。
Guidelines for designing data structures for concurrency
设计线程安全的数据结构需要考虑两个方面:安全的访问和真正的并发访问。第三章涉及了其中一些原则: * 当一个线程的操作破坏了不变量的时候,没有其他线程能够看到这个中间状态。 * 精心设计接口,避免接口自身存在的一些竞争,提供完整的操作接口而不是一系列接口。 * 当异常出现的时候,确保不变量不会被打破。 * 严格限制锁的范围和避免嵌套锁,以最小化死锁风险。
在开始任何细节之前,要考虑清楚要如何限制用户的使用:如果一个函数正在被访问,那么哪些其他的函数能够同时被访问。
构造函数和析构函数需要排他性。在构造完成之前或者开始析构之后,不应该使用这个对象。如果数据结构提供赋值、swap
、拷贝构造等,作为设计者,需要决定在这个过程中,调用其他函数是否是安全的。
第二个问题是真正的并发,下面是一些设计者需要考虑的问题: * 锁的范围是否受限?一个操作的部分是否能够移到锁的外部? * 数据结构的不同部分是不是可以用不同的互斥来保护? * 所有的操作都需要同样级别的保护? * 增加并发的修改是否会影响操作的语义?
所有问题的本质就是最小化串行化发生的范围,最大化并发。大部分的数据结构都是可以并发的只读和排他的修改,使用 std::shared_mutex
就能达到目的。很快会看到,支持并发的调用不同的操作也是一种很常见的场景。