多线程环境设计数据结构相比单线程,需要额外注意的是利用多线程提升并发度同时保持数据结构不变性,即满足如下两个原则:
1、正确性,保证多线程并发访问没有 data race
2、性能,保护最小的数据,提供最大的性能
《c++并发编程实战》提供了一种【无锁数据结构】,注意这里无锁的含义不是真正无锁,而是利用数据结构特性保证运行时并发抢锁的线程数量最小,达到一种常态下访问数据结构不被锁阻塞的状态
使用场景:
1、读远多于写
2、数据小,一般最大为几K字节的元数据
3、数据可以应用到一致性状态机,即byte级别判断为相等的两份数据,执行同种操作后仍保持一致
和传统读写锁的区别和优势:single unix的读写锁要求有线程在获取写锁阻塞时,后续读锁阻塞,从而防止写锁被饿死,这样造成了一个后果,若有写请求到来,则写操作完成前,无法处理新的读请求,从而造成系统【颠簸】,在有写请求的时候对外表现为性能下降,读延迟增加。DoublyBufferData将数据保存两份,分成前端和后端,读请求读前端,写请求到来写后端,用c++11 memory_order语义保证写请求完成后新的读请求可以立刻读取最新的结果同时无data race,是一种典型的【空间换时间】模式
数据结构图示:
原理描述:
1、DoublyBufferedData中包含foreground和background两份数据,index值为0或1,指示读请求应该访问的数据,如index为0表示此时应该读取data0,data0是只读的,因此多线程访问不需要并发保护,这里是无锁的
2、当有线程请求修改,首先根据 !index 获取background并做修改,background没有读者,因此这里可以放心修改,修改完成后,data0是旧数据,data1是新数据,使用memory_order_release语义修改index = !index,这样后续使用memory_order_acquire语义访问index的线程可以看到修改(inter-thread的sychronization-with关系)
3、对于此前正在访问data0的线程,通过遍历wrappers可以得到这些线程各自对应的wrapper,进而等待所有这些线程访问结束
4、到这里,所有之前读data0的请求都结束了,所有后续的读请求都请求到data1,此时可以放心的修改data0,从而完成一次修改操作
代码:
1、DoublyBufferedDataWrapperBase:
提供线程私有存储的ABC类,T是数据类型,TLS用于线程私有存储,可用于保存上下文,没有特殊要求不需要使用TLS
template <typename T, typename TLS>
class DoublyBufferedDataWrapperBase {
public:
TLS& user_tls() { return _user_tls; }
protected:
TLS _user_tls;
};
2、Wrapper:
每个线程使用一个