C#算法之基于无锁的C#并发队列实现

最近开始学习无锁编程,和传统的基于Lock的算法相比,无锁编程具有其独特的优点,Angel Lucifer 的关于无锁编程一文对此有详细的描述。

无锁编程的目标是在不使用Lock的前提下保证并发过程中共享数据的一致性,其主要的实现基础是CAS 操作,也就是compare_and_swap,通过处理器提供的指令,可以原子地更新共享数据,并同时监测其他线 程的干扰,.Net中的对应实现是InterLocked.CompareExchange函数。

既然不使用Lock,那在无锁编程中要时刻注意的是,代码可能在任意语句中被中断。如果是单个变量 ,我们可以使用 InterLocked.XXX 保证操作的原子性,但是如果有多个操作要完成的话,简单地组合 InterLocked.XXX 是远远不够的。通常的原则是对函数中用到的共享变量,先在代码开始处用局部变量保 存它的内容,在后面更新共享变量时,使用前述变量来判断其是否发生了改变,如果共享变量发生了改变 ,那么我们可能需要重试,或者在某些可能的情况下,当前线程可以"帮助"其他更新中的线程完成更新。

从上面可以总结出无锁算法的两个基本特征:

1. 无锁算法总是包含一个循环结构,以保证更新失败后重试

2. 无锁算法在更新共享变量时,总是使用CAS和原始值进行比较,以保证没有冲突

下面是按照Michael-Scott算法实现的并发队列,其中的Dequeue算法在IBM的非阻塞算法一文中有详细 介绍。代码如下:

Code

1public class ConcurrentLinkedQueue<T>
2{
3  private class Node<K>
4  {
5    internal K Item;
6    internal Node<K> Next;
7
8    public Node(K item, Node<K> next)
9    {
10      this.Item = item;
11      this.Next = next;
12    }
13  }
14
15  private Node<T> _head;
16  private Node<T> _tail;
17
18  public ConcurrentLinkedQueue()
19  {
20    _head = new Node<T>(default(T), null);
21    _tail = _head;
22  }
23
24  public bool IsEmpty
25  {
26    get { return (_head.Next == null); }
27  }
28
29  public void Enqueue(T item)
30  {
31    Node<T> newNode = new Node<T>(item, null);
32    while (true)
33    {
34      Node<T> curTail = _tail;
35      Node<T> residue = curTail.Next;
36
37      //判断_tail是否被其他process改变
38      if (curTail == _tail)
39      {
40        //A 有其他rocess执行C成功,_tail应该指向新的节点
41        if (residue == null)
42        {
43          //C 其他process改变了tail节点,需要重新取tail节点
44          if (Interlocked.CompareExchange<Node<T>>(
45            ref curTail.Next, newNode, residue) == residue)
46          {
47            //D 尝试修改tail
48            Interlocked.CompareExchange<Node<T>>(ref _tail, newNode, curTail);
49            return;
50          }
51        }
52        else
53        {
54          //B 帮助其他线程完成D操作
55          Interlocked.CompareExchange<Node<T>>(ref _tail, residue, curTail);
56        }
57      }
58    }
59  }
60
61  public bool TryDequeue(out T result)
62  {
63    Node<T> curHead;
64    Node<T> curTail;
65    Node<T> next;
66    do
67    {
68      curHead = _head;
69      curTail = _tail;
70      next = curHead.Next;
71      if (curHead == _head)
72      {
73        if (next == null) //Queue为空
74        {
75          result = default(T);
76          return false;
77        }
78        if (curHead == curTail) //Queue处于Enqueue第一个node的过程中
79        {
80          //尝试帮助其他Process完成操作
81          Interlocked.CompareExchange<Node<T>>(ref _tail, next, curTail);
82        }
83        else
84        {
85          //取next.Item必须放到CAS之前
86          result = next.Item;
87          //如果_head没有发生改变,则将_head指向next并退出
88          if (Interlocked.CompareExchange<Node<T>>(ref _head,
89            next, curHead) == curHead)
90            break;
91        }
92      }
93    }
94    while (true);
95    return true;
96  }
97}
98

根据自己的测试(双核CPU),在轻度和中度争用情况下,无锁算法比基于锁的算法性能好很多,在争 用非常严重的情况下(100个并发线程以上/每CPU),基于锁的算法性能开始显示出优势,因为一旦发生 争用,基于锁的算法会立刻切换到其他线程,而无锁算法会进入下一次循环,导致CPU的占用。但是如此 严重的争用在实际中并不多见,并且可以采用SpinWait的方法加以改进。基于锁的算法在测试中曾经出现 过类似死锁的现象,无锁算法则完全没有出过类似问题,另外,处理器核心越多,基于锁的算法效率越差 。

从上面的算法实现中,可以体会到无锁算法的优势:在并发的多个线程中,总是有线程能够推进,算 法总能在有限的循环次数内完成,并且在某些冲突的情况下,一个线程可以“帮助”其他线程完成被中断 的工作,这些对提高吞吐量都有很大的作用。

时间: 2024-12-30 22:34:02

C#算法之基于无锁的C#并发队列实现的相关文章

无锁和无等待的定义和例子

原文链接,译文连接,译者:周可人,校对:梁海舰 在查阅google之后,我发现没有一处对并发算法或是数据结构规定的演进条件(progress condition,注:参考[1],译者认为翻译为演进状态更为合适)做合理的解释.甚至在"The Art of Multiprocessor Programming"中也只有围绕书本的一小段定义,大部分定义是单行的句子,因而造成了我们普通人含义模糊的理解,所以在这里我把我对这些概念的理解整理在一起,并且在每一种概念后面给出相应的例子. 我们先将演

无锁并发和无等待并发的对比分析

原文地址:作者:rethinkdb  译者:sooerr 校对:方腾飞 有两种非阻塞线程同步算法,即无锁和无等待,这两种算法经常会产生混淆. 在无锁系统中,当任何特定的运算被阻塞的时候,所有CPU可以继续处理其他的运算.换种方式说,在无锁系统中,当给定线程被其他线程阻塞的时候,所有CPU可以不停的继续处理其他工作.无锁算法大大增加系统整体的吞吐量,因为它只偶尔会增加一定的交易延迟.大部分高端数据库系统是基于无锁算法而构造的,以满足不同级别. 相反,无等待算法保证了所有CPU在连续处理有效工作的时

使用CAS实现无锁的SkipList

感谢同事[付哲]发布此文. 无锁 并发环境下最常用的同步手段是互斥锁和读写锁,例如pthread_mutex和pthread_readwrite_lock,常用的范式为: void ConcurrencyOperation() { mutex.lock(); // do something mutex.unlock(); } 这种方法的优点是: 编程模型简单,如果小心控制上锁顺序,一般来说不会有死锁的问题: 可以通过调节锁的粒度来调节性能. 缺点是: 所有基于锁的算法都有死锁的可能: 上锁和解锁

基于锁的并发算法 vs 无锁的并发算法

原文链接 作者:Martin Thompson  译者:曹姚君 校对: 方腾飞 上周在由Heinz Kabutz通过JCrete 组织的开放空间会议(unconference)上,我参加一个新的java规范 JSR166 StampedLock的审查会议.StampedLock 是为了解决多个readers 并发访问共享状态时,系统出现的内存地址竞争问题.在设计上通过使用乐观的读操作,StampedLock 比ReentrantReadWriteLock 更加高效: 在会议期间,我突然意思到两点

PgSQL · 源码分析 · PG中的无锁算法和原子操作应用一则

原子操作概述 近年来随着服务器上CPU核数的不断增加,无锁算法(Lock Free)越来越广泛的被应用于高并发的系统中.PostgreSQL 做为世界上最高级开源数据库也在9.5时引入了无锁算法.本文先介绍了无锁算法和原子操作在PostgreSQL中的具体实现, 再通过一个Patch来看一下在PostgreSQL中是如何利用它来解决实际的高并发问题的. 无锁算法是利用CPU的原子操作实现的数据结构和算法来解决原来只能用锁才能解决的并发控制问题. 众所周知,在一个并发系统中特别是高并发的场景下,锁

高效线程池之无锁化实现(Linux C)

笔者之前照着通用写法练手写过一个小的线程池版本,最近几天复习了一下,发现大多数线程池实现都离不开锁的使用,如互斥量pthread_mutex*结合条件变量pthread_cond*.众所周知,锁的使用对于程序性能影响较大,虽然现有的pthread_mutex*在锁的申请与释放方面做了较大的优化,但仔细想想,线程池的实现是可以做到无锁化的,于是有了本文. 1.常见线程池实现原理 如上图所示,工作队列由主线程和工作者线程共享,主线程将任务放进工作队列,工作者线程从工作队列中取出任务执行.共享工作队列

透过 Linux 内核看无锁编程

非阻塞型同步 (Non-blocking Synchronization) 简介 如何正确有效的保护共享数据是编写并行程序必须面临的一个难题,通常的手段就是同步.同步可分为阻塞型同步(Blocking Synchronization)和非阻塞型同步( Non-blocking Synchronization). 阻塞型同步是指当一个线程到达临界区时,因另外一个线程已经持有访问该共享数据的锁,从而不能获取锁资源而阻塞,直到另外一个线程释放锁.常见的同步原语有 mutex.semaphore 等.如

Disruptor(无锁并发框架)-发布

原文:http://blog.codeaholics.org/2011/the-disruptor-lock-free-publishing/ 译者:罗立树 假如你生活在另外一个星球,我们最近开源了一套高性能的基于消息传递的开源框架. 下面我给大家介绍一下如何将消息通过Ring buffer在无锁的情况下进行处理. 在深入介绍之前,可以先快速阅读一下Trish发表的文章,该文章介绍了ring buffer和其工作原理. 这篇文章的要点如下: 1.ring buffer是由一个大数组组成的. 2.

Java 高并发四:无锁详细介绍_java

在[高并发Java 一] 前言中已经提到了无锁的概念,由于在jdk源码中有大量的无锁应用,所以在这里介绍下无锁. 1 无锁类的原理详解 1.1 CAS CAS算法的过程是这样:它包含3个参数CAS(V,E,N).V表示要更新的变量,E表示预期值,N表示新值.仅当V 值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么 都不做.最后,CAS返回当前V的真实值.CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成 操作.当多个线程同时使用CAS操