无锁数据结构三:无锁数据结构的两大问题

实现无锁数据结构最困难的两个问题是ABA问题和内存回收问题。它们之间存在着一定的关联:一般内存回收问题的解决方案,可以作为解决ABA问题的一种只需很少开销或者根本不需额外开销的方法,但也存在一些情况并不可行,如两个链表实现的栈,不断在两个栈间交换节点。下面对两个问题的主流解决方法进行介绍。

标签指针(Tagged pointers)

标签指针作为一种规范由IBM引入,旨在解决ABA问题。从某一方面来看,ABA问题的出现是由于不能区分前一个A与后一个A,标签指针通过标签(版本号)来解决。其每个指针由一组原子性的内存单元和标签(32比特的整数)组成。

 
  1. template <typename T>

  2. struct tagged_ptr {

  3. T * ptr ;

  4. unsigned int tag ;

  5. tagged_ptr(): ptr(nullptr), tag(0) {}

  6. tagged_ptr( T * p ): ptr(p), tag(0) {}

  7. tagged_ptr( T * p, unsigned int n ): ptr(p), tag(n){}

  8. T * operator->() const { return ptr; }

  9. };

标签作为一个版本号,随着标签指针上的每次CAS运算增加,并且只增不减。一旦需要从容器中非物理地移除某个元素,就应将其放入一个放置空闲元素的列表中。在空闲元素列表中,逻辑删除的元素完全有可能被再次调用。因为是无锁数据结构,一个线程删除X元素,另外一个线程依然可以持有标签指针的本地副本,并指向元素字段。因此需要一个针对每种T类型的空闲元素列表。多数情况下,将元素放入空闲列表中,意味着调用这个T类型数据的析构函数是非法的(考虑到并行访问,在析构函数运算的过程中,其它线程是可以读到此元素的数据)。

有了标签,A在CAS时虽然内存地址相同,但若A已被使用过,其标签不同。不会出现ABA问题。

当然其必然也有缺点:

其一:要实现标签指针需要平台支持,平台需要支持dwCAS(地址指针需一个word,标签至少需32位),由于现代的系统架构都有一套完整的64位指令集,对于32位系统,dwCAS需要64位,可以支持;但对于64位系统,dwCAS需要128位(至少96位),并不是所有架构都能支持。

The scheme is implemented at platforms, which have an atomic CAS primitive over a double word (dwCAS). This requirement is fulfilled for 32-bit modern systems as dwCAS operates with 64-bit words while all modern architectures have a complete set of 64-bit instructions. But 128-bit (or at least 96-bit) dwCAS is required for 64-bit operation mode. It isn’t implemented in all architectures.

其二:空闲列表通常以无锁栈或无锁队列的方式实现,对性能也会有影响,但也正是因为使用无锁,导致其性能有提高(相对于有锁)。 
其三: 对于每种类型提供单独的空闲列表,这样做太过奢侈难以被大众所接收,一些应用使用内存太过低效。例如,无锁队列通常包含10个元素,但可以扩展到百万,比如在一次阻塞后,空闲列表扩展至百万。

Availability of a separate free-list for every data type can be an unattainable luxury for some applications as it can lead to inefficient memory use. For example, if a lock-free queue consists of 10 elements on the average but its size can increase up to 1 million, the free-list size after the spike can be about 1 million. Such behavior is often illegal.

Example

详情可参考1

 
  1. template <typename T> struct node {

  2. tagged_ptr next;

  3. T data;

  4. } ;

  5. template <typename T> class MSQueue {

  6. tagged_ptr<T> volatile m_Head;

  7. tagged_ptr<T> volatile m_Tail;

  8. FreeList m_FreeList;

  9. public:

  10. MSQueue()

  11. {

  12. // Allocate dummy node

  13. // Head & Tail point to dummy node

  14. m_Head.ptr = m_Tail.ptr = new node();

  15. }

  16. void enqueue( T const& value )

  17. {

  18. E1: node * pNode = m_FreeList.newNode();

  19. E2: pNode–>data = value;

  20. E3: pNode–>next.ptr = nullptr;

  21. E4: for (;;) {

  22. E5: tagged_ptr<T> tail = m_Tail;

  23. E6: tagged_ptr<T> next = tail.ptr–>next;

  24. E7: if tail == Q–>Tail {

  25. // Does Tail point to the last element?

  26. E8: if next.ptr == nullptr {

  27. // Trying to add the element in the end of the list

  28. E9: if CAS(&tail.ptr–>next, next, tagged_ptr<T>(node, next.tag+1)) {

  29. // Success, leave the loop

  30. E10: break;

  31. }

  32. E11: } else {

  33. // Tail doesn’t point to the last element

  34. // Trying to relocate tail to the last element

  35. E12: CAS(&m_Tail, tail, tagged_ptr<T>(next.ptr, tail.tag+1));

  36. }

  37. }

  38. } // end loop

  39. // Trying to relocate tail to the inserted element

  40. E13: CAS(&m_Tail, tail, tagged_ptr<T>(pNode, tail.tag+1));

  41. }

  42. bool dequeue( T& dest ) {

  43. D1: for (;;) {

  44. D2: tagged_ptr<T> head = m_Head;

  45. D3: tagged_ptr<T> tail = m_Tail;

  46. D4: tagged_ptr<T> next = head–>next;

  47. // Head, tail and next consistent?

  48. D5: if ( head == m_Head ) {

  49. // Is queue empty or isn’t tail the last?

  50. D6: if ( head.ptr == tail.ptr ) {

  51. // Is the queue empty?

  52. D7: if (next.ptr == nullptr ) {

  53. // The queue is empty

  54. D8: return false;

  55. }

  56. // Tail isn’t at the last element

  57. // Trying to move tail forward

  58. D9: CAS(&m_Tail, tail, tagged_ptr<T>(next.ptr, tail.tag+1>));

  59. D10: } else { // Tail is in position

  60. // Read the value before CAS, as otherwise

  61. // another dequeue can deallocate next

  62. D11: dest = next.ptr–>data;

  63. // Trying to move head forward

  64. D12: if (CAS(&m_Head, head, tagged_ptr<T>(next.ptr, head.tag+1))

  65. D13: break // Success, leave the loop

  66. }

  67. }

  68. } // end of loop

  69. // Deallocate the old dummy node

  70. D14: m_FreeList.add(head.ptr);

  71. D15: return true; // the result is in dest

  72. }

险象指针(Hazard pointer)

此规则由Michael创建(Hazard Pointers: Safe Memory Reclamation for Lock-Free Objects),用来保护无锁数据结构中元素的局部引用,也即延迟删除。

it’s the most popular and studied scheme of delayed deletion.

此规则的实现仅依赖原子性读写,而未采用任何重量级的CAS同步原语。其实现原理大致如下:

  1. 通过使用线程局部数据保存正在使用的共享内存(数据结构元素),以及要删除的共享内存,记为Plist,Dlist。
  2. 每个线程都将自己正在访问其不希望被释放的内存对象保存在Plist中,使用完取出。
  3. 当任何线程删除内存对象,把对象放入Dlist。
  4. 当Dlist中元素数量达到阈值,扫描Dlist和Plist,真正释放在Dlist中有而所以Plist中没有的元素。

Note:Plist 一写多读;Dlist单写单读。

hazard pointer的使用是要结合具体的数据结构的,我们需要分析所要保护的数据结构的每一步操作,找出需要保护的内存对象并使用hazard pointer替换普通指针对危险的内存访问进行保护。

Example

详情可参考3

 
  1. template <typename T>

  2. void Queue<T>::enqueue(const T &data)

  3. {

  4. qnode *node = new qnode();

  5. node->data = data;

  6. node->next = NULL;

  7. // qnode *t = NULL;

  8. HazardPointer<qnode> t(hazard_mgr_);

  9. qnode *next = NULL;

  10.  
  11. while (true) {

  12. if (!t.acquire(&tail_)) {

  13. continue;

  14. }

  15. next = t->next;

  16. if (next) {

  17. __sync_bool_compare_and_swap(&tail_, t, next);

  18. continue;

  19. }

  20. if (__sync_bool_compare_and_swap(&t->next, NULL, node)) {

  21. break;

  22. }

  23. }

  24. __sync_bool_compare_and_swap(&tail_, t, node);

  25. }

  26.  
  27. template <typename T>

  28. bool Queue<T>::dequeue(T &data)

  29. {

  30. qnode *t = NULL;

  31. // qnode *h = NULL;

  32. HazardPointer<qnode> h(hazard_mgr_);

  33. // qnode *next = NULL;

  34. HazardPointer<qnode> next(hazard_mgr_);

  35.  
  36. while (true) {

  37. if (!h.acquire(&head_)) {

  38. continue;

  39. }

  40. t = tail_;

  41. next.acquire(&h->next);

  42. asm volatile("" ::: "memory");

  43. if (head_ != h) {

  44. continue;

  45. }

  46. if (!next) {

  47. return false;

  48. }

  49. if (h == t) {

  50. __sync_bool_compare_and_swap(&tail_, t, next);

  51. continue;

  52. }

  53. data = next->data;

  54. if (__sync_bool_compare_and_swap(&head_, h, next)) {

  55. break;

  56. }

  57. }

  58.  
  59. /* h->next = (qnode *)1; // bad address, It's a trap! */

  60. /* delete h; */

  61. hazard_mgr_.retireNode(h);

  62. return true;

  63. }

参考资料

  1. 无锁数据结构(机制篇):内存管理规则 (EN 原文)
  2. 风险指针(Hazard Pointers)——用于无锁对象的安全内存回收机制
  3. http://blog.kongfy.com/2017/02/hazard-pointer/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值