基于锁的并发数据结构
并发计数器
最简单的数据结构是计数器。此数据结构经常使用且接口简单。
简单非并发计数器:
线程安全计数器:
这个并发计数器简单有效。它遵循了常见的最简单也是最基本的并发数据结构设计模式:简单地加上一个锁,即在操作该数据结构时上锁,在返回时解锁。
上面的计数器虽然保证了线程安全,但是性能方面扩展性很差,在多核CPU上运行速度很差。
性能问题可以通过sloppy counter方法来解决。sloppy counter通过多个局部物理计数器代表一个全局逻辑计数器,每个核都有一个局部物理计数器。具体的,一个4CPU的机器,有四个局部计数器和一个全局计数器。除这些计数器外,还有锁,每个局部计数器和全局计数器各自都有一把锁。
sloppy counter的基本思想如下:当运行在某个核上的某个线程想要加计数器时,它就增加核的局部计数器;通过对应的局部锁访问这个同步局部计数器。由于每个CPU都有自己的局部计数器,多CPU上的多线程就可以无竞争地更新局部计数器,因此计数器的更新是可以扩展的。
然而,为了保持全局计数器保持最新,局部值会定期提交给全局计数器,通过获取全局锁访问全局计数器并加以局部计数器的值;然后局部计数器的置重置为0.
并发链表
如上图所示,该代码在插入函数入口获取锁,在退出时释放锁。如果malloc()碰巧失败的话(极少数情况下),这个问题就会很棘手:这种情况下,仍然必须在插入失败之前释放锁。
我们只要稍微修改一下insert中的代码就可以解决问题。
我们将上锁的位置放在了malloc()函数之后,在操作共享链表的时候加锁,在插入完成之后再解锁,这样就完成了并发插入。
扩展链表:
研究人员开发出一个可以使链表并发性提高的技术:handover-hand locking(即:锁耦合)
上述技术的思想很简单。并非整个链表拥有一个锁,而是每个节点各自拥有一个锁。当遍历这个链表时,需要先获取下一个节点的锁在释放当前节点的锁。这种链表是有一定意义的:它使链表操作有了高度的并发性。但是实际上让这样的链表运行的比简单方法快还是很难的,因为在一次链表遍历中获取,释放每个节点锁的开销还是很大的。即便是非常大的链表以及大量的线程,这种方法的并发性也不太可能比简单方法的并发性高。
TIP:
并发队列
最简单的方法就是给队列加一把大锁。
Michael和Scott提出了一种并发队列
上面的并发队列使用了两个锁,一个是队首锁,一个是队尾锁,设置这两个锁是为了入队和出队的并发操作。入队函数仅访问队尾的锁,出队函数仅访问队首的锁。
并发哈希表
并发哈希表的设计是在并发链表的基础上构造,每个节点都有一个锁,这样就可以进行多个并发操作。
我们将使用一个锁的链表和并发哈希表做并发性能测试,发现简单的并发哈希表的扩展性非常好,但链表的扩展性很差,结果如下图:
TIPS: