29.Lock-based Concurrent Data Structures


29. Lock-based Concurrent Data Structures

  • 有了lock之后,就可以用在一些数据结构中,使其变得thread safe线程安全。问题来了:给定一个数据结构时,如何加入lock使其能够正常工作?如何加入lock才能有好的性能,使得多线程可以尽可能多地并发访问结构?

29.1 Concurrent Counters

  • 以一个最简单的counter数据结构开始:

  • 如何将上面的counter变成线程安全的counter?

可以看到,在counter中加入了lock,并且每次读写数据时都需要对其进行加锁。

  • 让我们来康康现在这个线程安全的lock性能如何。

上图(Precise)展示了从1到4个线程,每个线程更新counter一百万次,实验环境是Intel2.7GHz I5 CPU。可以看到,随着线程数目的增大,该counter的性能逐渐变差。

  • Scalable Counting

如何解决上述多线程时性能价差的问题呢?其中一种方法叫做approximate counter。approximate counter对于每一个CPU都会设置一个局部物理计数器,并且还有一个全局计数器。对于这些计数器,相应地每个计数器都要有一个lock。

基本思路是:一个运行的线程要更新计数器时,更新的是其所在CPU对应的计数器,由于每个CPU对应计数器都有一个lock,因此在一个CPU上更新一定是同步的。因为每个CPU都有自己的计数器去,那么多CPU之间的线程,可以无连接的更新局部计数器,因此是scalable的。

但是,如何保证全局计数器保存的数值是最新的呢?局部计数器要定期把自己保存的值转移到全局计数器上,因此需要申请全局lock然后将自己的值加到全局计数器上,最后把自己的值重置为0。

问题又来了,将局部计数器的值转移到全局计数器上的频率是多少呢?这是由一个阈值S决定的,当局部计数器的值超过S,就转移。如果S较小,那么计数器可能就没有那么的scalable;如果S较大,计数器更scalable,但是可能计数值可能就没那么准确。

如果S=5:

代码长这样:

还是来康康性能,S值为1024:

可以看到,Approximate性能非常好。

下图展示了四个线程,每个线程更新一百万次,S对性能的影响:

29.2 Concurrent Linked Lists

  • 现在来康康稍微复杂一点点的,链表。加入lock后初级版本长这样:

可以看到,在对链表进行读写操作前,先申请lock,在所有操作都结束后,再释放lock。

这种做法是有大概40%几率出bug的,咱也不知道会出什么bug,但显然这种方法就是不行。

因此,稍稍做一些调整,让对于锁的申请和释放只在真正的critical section前后进行,并且假设malloc()本身就是线程安全的:

  • Scaling Linked Lists

上面这个版本可以实现基本的线程安全了,但是还不够scalable。一种解决办法叫做hand-over-hand locking。思想非常简单,不再给每个链表一个lock,而是给链表中的每一个Node一个lock。对链表遍历时,必须先获取下一个node的lock,再释放当前node的lock。

29.3 Concurrent Queues

  • 其实队列和链表也差不多,最简单的一种方式实现就是加一把大的lock,这里就不再赘述了。这一节来看看一种并发性更好的实现方式,代码长这样:

和普通加一把大lock不同的是,这里加了两个lock,队头队尾分别一个,这么做的目的就是提高入队操作和出队操作的并发性。并且,在这里加了一个dummy node,目的是为了将头尾操作分开。

这里的实现方式其实还是有问题的,并不能满足全部关于队列的使用需求。

29.4 Concurrent Hash Table

  • 线程安全的hash table,使用之前的线程安全的链表。每个BUCKETS一把lock,因此性能较好。

与链表的性能对比图:

29.5 Summary

  • 主要介绍了如何构建常见的线程安全的数据结构,链表、队列及hash table。

文章作者: foursevenlove
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 foursevenlove !
 上一篇
30.Condition Variables 30.Condition Variables
30. Condition Variables 之前介绍了lock,以及使用lock构建的一些线程安全的数据结构。但是那并不是所有并发的内容,说到并发,怎么能不谈谈同步呢?并发中,一个线程等待另一个线程是很常见的事情。举个栗子,父线程要等待
下一篇 
28.Locks 28.Locks
28. Locks 并发中最基本的问题就是如何保证一段代码执行的原子性,这一章的lock就是用来解决这个问题的。
  目录