Lock-Free 编程
文章索引
Lock-Free 编程是什么?
当谈及 Lock-Free 编程时,我们常将其概念与 Mutex 或 Lock 联系在一起,描述要在编程中尽量少使用这些锁结构,降低线程间互相阻塞的机会,以提高应用程序的性能。类同的概念还有 "Lockless" 和 "Non-Blocking" 等。实际上,这样的描述只涵盖了 Lock-Free 编程的一部分内容。本质上说,Lock-Free 编程仅描述了代码所表述的性质,而没有限定或要求代码该如何编写。
基本上,如果程序中的某一部分符合下面的条件判定描述,则我们称这部分程序是符合 Lock-Free 的。反过来说,如果某一部分程序不符合下面的条件描述,则称这部分程序是不符合 Lock-Free 的。

从这个意义上来说,Lock-Free 中的 "Lock" 并没有直接涉及 Mutex 或 Lock 等互斥量结构,而是描述了应用程序因某种原因被锁定的可能性,例如可能因为死锁(DeadLock)、活锁(LiveLock)或线程调度(Thread Scheduling)导致优先级被抢占等。
Lock-Free 编程的一个重要效果就是,在一系列访问 Lock-Free 操作的线程中,如果某一个线程被挂起,那么其绝对不会阻止其他线程继续运行(Non-Blocking)。
下面的这段简单的程序片段中,没有使用任何互斥量结构,但却不符合 Lock-Free 的性质要求。如果用两个线程同时执行这段代码,在线程以某种特定的调度方式运行时,非常有可能两个线程同时陷入死循环,也就是互相阻塞了对方。
while (x == )
{
x = - x;
}
所以说,Lock-Free 编程所带来的挑战不仅来自于其任务本身的复杂性,还要始终着眼于对事物本质的洞察。
通常,应该没有人会期待一个大型的应用程序中全部采用 Lock-Free 技术,而都是在有特定需求的类的设计上采用 Lock-Free 技术。例如,如果需要一个 Stack 类应对多线程并发访问的场景,可以使用 Lock-Free 相关技术实现 ConcurrentStack 类,在其 Push 和 Pop 操作中进行具体的实现。所以,在使用 Lock-Free 技术前,需要预先考虑一些软件工程方面的成本:
- Lock-Free 技术很容易被错误的使用,代码后期的维护中也不容易意识到,所以非常容易引入 Bug,而且这样的 Bug 还非常难定位。
- Lock-Free 技术的细节上依赖于内存系统模型、编译器优化、CPU架构等,而这在使用 Lock 机制时是不相关的,所以也增加了理解和维护的难度。
Lock-Free 编程技术
当我们准备要满足 Lock-Free 编程中的非阻塞条件时,有一系列的技术和方法可供使用,如原子操作(Atomic Operations)、内存栅栏(Memory Barrier)、避免 ABA 问题(Avoiding ABA Problem)等。那么我们该如何抉择在何时使用哪种技术呢?可以根据下图中的引导来判断。

读改写原子操作(Atomic Read-Modify-Write Operations)
原子操作(Atomic Operations)在操作内存时可以被看做是不可分割的(Indivisible),其他线程不会打断该操作,没有操作只被完成一部分之说。在现代的 CPU 处理器上,很多操作已经被设计为原子的,例如对齐读(Aligned Read)和对齐写(Aligned Write)等。
Read-Modify-Write(RMW)操作的设计则让执行更复杂的事务操作变成了原子的,使得当有多个写入者想对相同的内存进行修改时,保证一次只执行一个操作。
例如,常见的对整型值进行加法操作的 RMW 操作:
- 在 Win32 中有 _InterlockedIncrement
- 在 iOS 中有 OSAtomicAdd32
- 在 C++11 中有 std::atomic<int>::fetch_add
RMW 操作在不同的 CPU 家族中是通过不同的方式来支持的。
- x86/64 和 Itanium 架构通过 Compare-And-Swap (CAS) 方式来实现;
- PowerPC、MIPS 和 ARM 架构通过 Load-Link/Store-Conditional (LL/SC) 方式来实现;
例如在 x86 架构下,通过 LOCK 指令前缀可以使许多指令操作(ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG)变成原子操作,其中 CMPXCHG 指令可用于实现 CAS 操作。

下面是使用 LOCK 和 CMPXCHG 来实现 CAS 操作的代码示例。
__inline int CAS(volatile int & destination, int exchange, int comperand)
{
__asm {
MOV eax, comperand
MOV ecx, exchange
MOV edx, destination
LOCK CMPXCHG[edx], ecx /* 如果eax与edx相等, 则ecx送edx且ZF置1;
否则edx送ecx, 且ZF清0.*/
}
} /* Accumulator = AL, AX, EAX, or RAX depending on
whether a byte, word, doubleword, or
quadword comparison is being performed */ IF accumulator = DEST
THEN
ZF ← ;
DEST ← SRC;
ELSE
ZF ← ;
accumulator ← DEST;
FI;
Compare-And-Swap 循环(CAS Loops)
在 Win32 平台上,CAS 操作有一组原生的实现,例如 _InterlockedCompareExchange等。对 RMW 操作最常见的讨论可能就是,如何通过 CAS Loops 来完成对事务的原子处理。
通常,开发人员会设计在一个循环中重复地执行 CAS 操作以试图完成一个事务操作。这个过程分为 3 步:
- 从指定的内存位置读取原始的值;
- 根据读取到的原始的值计算出新的值;
- 检测如果内存位置仍然是原始的值时,则将新值写入该内存位置;
例如,向 LockFreeStack 中压入新的节点:
void LockFreeStack::Push(Node* newHead)
{
for (;;)
{
// Read the original value from a memory location.
// Copy a shared variable (m_Head) to a local.
Node* oldHead = m_Head; // Compute the new value to be set.
// Do some speculative work, not yet visible to other threads.
newHead->next = oldHead; // Set the new value only if the memory location is still the original value.
// Next, attempt to publish our changes to the shared variable.
// If the shared variable hasn't changed, the CAS succeeds and we return.
// Otherwise, repeat.
if (_InterlockedCompareExchange(&m_Head, newHead, oldHead) == oldHead)
return;
}
}
上面代码中的循环操作仍然符合 Lock-Free 条件要求,因为如果 _InterlockedCompareExchange 条件测试失败,也就意味着另外的线程已经成功修改了值,而当前线程可以再下一个循环周期内继续判断以完成操作。
ABA 问题(ABA Problem)
在实现 CAS Loops 时,当存在多个线程交错地对共享的内存地址进行处理时,如果实现设计的不正确,将有可能遭遇 ABA 问题。
若线程对同一内存地址进行了两次读操作,而两次读操作得到了相同的值,通过判断 "值相同" 来判定 "值没变"。然而,在这两次读操作的时间间隔之内,另外的线程可能已经修改了该值,这样就相当于欺骗了前面的线程,使其认为 "值没变",实际上值已经被篡改了。
下面是 ABA 问题发生的过程:
- T1 线程从共享的内存地址读取值 A;
- T1 线程被抢占,线程 T2 开始运行;
- T2 线程将共享的内存地址中的值由 A 修改成 B,然后又修改回 A;
- T1 线程继续执行,读取共享的内存地址中的值仍为 A,认为没有改变然后继续执行;
由于 T1 并不知道在两次读取的值 A 已经被 "隐性" 的修改过,所以可能产生无法预期的结果。
例如,使用 List 来存放 Item,如果将一个 Item 从 List 中移除并释放了其内存地址,然后重新创建一个新的 Item,并将其添加至 List 中,由于优化等因素,有可能新创建的 Item 的内存地址与前面删除的 Item 的内存地址是相同的,导致指向新的 Item 的指针因此也等同于指向旧的 Item 的指针,这将引发 ABA 问题。

举个更生活化的例子:
土豪拿了一个装满钱的 Hermes 黑色钱包去酒吧喝酒,将钱包放到吧台上后,转头和旁边的朋友聊天,小偷趁土豪转头之际拿起钱包,将钱包里的钱取出来并放入餐巾纸保持钱包厚度,然后放回原处,小偷很有职业道德,只偷钱不偷身份证,土豪转过头后发现钱包还在,并且还是他熟悉的 Hermes 黑色钱包,厚度也没什么变化,所以土豪认为什么都没发生,继续和朋友聊天,直到结账时发现钱包中的钱已经被调包成餐巾纸。
所以,我觉得 ABA 问题还可以被俗称为 "调包问题"。那么怎么解决 "调包问题" 呢?土豪开始想办法了。
土豪想的第一个办法是,找根绳子将钱包绑在手臂上,要打开钱包就得先把绳子割断,割绳子就会被发现。这种做法实际上就是 Load-Link/Store-Conditional (LL/SC) 架构中所做的工作。
土豪想的另一个办法是,在钱包上安个显示屏,每次打开钱包显示屏上的数字都会 +1,这样当土豪在转头之前可以先记录下显示屏上的数字,在转过头后可以确认数字是否有变化,也就知道钱包是否被打开过。这种做法实际上就是 x86/64 架构中 Double-Word CAS Tagging 所做的工作。
土豪还担心小偷下次会不会买一个一模一样的钱包,直接调包整个钱包,这样连银行卡和身份证都丢了怎么办,土豪决定买一个宇宙独一无二的钱包,除非把它给烧了,否则就不会再有相同的钱包出现。这种做法实际上就是 Garbage Collection (GC) 所做的工作。
内存模型(Memory Model)对细粒度锁的影响
在多线程系统中,当多个线程同时访问共享的内存时,就需要一个规范来约束不同的线程该如何与内存交互,这个规范就称之为内存模型(Memory Model)。
顺序一致性内存模型(Sequential Consistency Memory Model)则是内存模型规范中的一种。在这个模型中,内存与访问它的线程保持独立,通过一个控制器(Memory Controller)来保持与线程的联系,以进行读写操作。在同一个线程内的,读写操作的顺序也就是代码指定的顺序。但多个线程时,读写操作就会与其他线程中的读写操作发生交错。

如上图中所示,Thread 1 中在写入 Value 和 Inited 的值,而 Thread 2 中在读取 Inited 和 Value 的值到 Ri 和 Rv 中。由于在内存控制器中发生重排(Memory Reordering),最终的结果可能有很多种情况,如下表所示。

顺序一致性内存模型非常的直观,也易于理解。但实际上,由于该模型在内存硬件实现效率上的限制,导致商用的 CPU 架构基本都没有遵循该模型。一个更贴近实际的多处理器内存模型更类似于下图中的效果。

也就是说,每个 CPU 核都会有其自己的缓存模型,例如上图中的 Level 1 Cache 和 Level 2 Cache,用以缓存最近使用的数据,以提升存取效率。同时,所有的写入数据都被缓冲到了 Write Buffer 缓冲区中,在数据在被刷新至缓存前,处理器可以继续处理其他指令。这种架构提升了处理器的效率,但同时也意味着我们不仅要关注 Memory,同时也要关注 Buffer 和 Cache,增加了复杂性。

上图所示为缓存不一致问题(Incoherent Caches),当主存(Main Memory)中存储着 Value=5,Inited=0 时,Processor 1 就存在着新写入 Cache 的值没有被及时刷新至 Memory 的问题,而 Processor 2 则存在着读取了 Cache 中旧值的问题。
显然,上面介绍着内存重排和缓存机制会导致混乱,所以实际的内存模型中会引入锁机制(Locking Protocol)。通常内存模型会遵循以下三个规则:
- Rule 1:当线程在隔离状态运行时,其行为不会改变;
- Rule 2:读操作不能被移动到获取锁操作之前;
- Rule 3:写操作不能被移动到释放锁操作之后;
Rule 3 保证了在释放锁之前,所有写入操作已经完成。Rule 2 保证要读取内存就必须先获取锁,不会再有其他线程修改内存。Rule 1 则保证了获得锁之后的操作行为是顺序的。
在体现锁机制(Locking Protocol)的价值的同时,我们也会意识到它所带来的限制,也就是限制了编译器和 CPU 对程序做优化的自由。
我们知道,.NET Framework 遵循 ECMA 标准,而 ECMA 标准中则定义了较为宽松的内存访问模型,将内存访问分为两类:
- 常规内存访问(Ordinary Memory Access)
- 易变内存访问(Volatile Memory Access)
其中,易变内存访问是特意为 "volatile" 设计,它包含如下两个规则:
- 读和写操作不能被移动到 volatile-read 之前;
- 读和写操作不能被移动到 volatile-write 之后;
对于那些没有使用 "lock" 和 "volatile" 的程序片段,编译器和硬件可以对常规内存访问做任何合理的优化。反过来讲,内存系统仅需在应对 "lock" 和 "volatile" 时采取缓存失效和刷新缓冲区等措施,这极大地提高了性能。

顺序一致性(Sequential Consistency)的要求描述了程序代码描述的顺序与内存操作执行的顺序间的关系。多数编程语言都提供顺序一致性的支持,例如在 C# 中可以将变量标记为 volatile。
A volatile read has "acquire semantics" meaning that the read is guaranteed to occur prior to any references to memory that occur after the read instruction in the CIL instruction sequence.
A volatile write has "release semantics" meaning that the write is guaranteed to happen after any memory references prior to the write instruction in the CIL instruction sequence.
下面的列表展示了 .NET 中内存读写操作的效果。
|
Construct |
Refreshes Thread Cache Before? |
Flushes Thread Cache After? |
Notes |
|
Ordinary Read |
No |
No |
Read of a non-volatile field |
|
Ordinary Write |
No |
Yes |
Write of a non-volatile field |
|
Volatile Read |
Yes |
No |
Read of volatile field, or Thread.VolitelRead |
|
Volatile Write |
No |
Yes |
Write of volatile field |
|
Thread.MemoryBarrier |
Yes |
Yes |
Special memory barrier method |
|
Interlocked Operations |
Yes |
Yes |
Increment, Add, Exchange, etc. |
|
Lock Acquire |
Yes |
No |
Monitor.Enter or entering a lock {} region |
|
Lock Release |
No |
Yes |
Monitor.Exit or exiting a lock {} region |
代码实践
我们需要在实践中体会 Lock-Free 编程,方能洞察机制的本质,加深理解。下面用实现栈 Stack 类的过程来完成对 Lock-Free 编程的探索。
栈结构实际上就是 FILO 先入后出队列,通常包括两个操作:
- Push:向栈顶压入一个元素(Item);
- Pop:从栈顶弹出一个元素(Item);
这里我们选用单链表结构(Singly Linked List)来实现 FILO 栈,每次入栈的都是新的链表头,每次出栈的也是链表头。

实现普通的栈 SimpleStack 类
构建一个内部类 Node 用于存放 Item,并包含 Next 引用以指向下一个节点。
private class Node<TNode>
{
public Node<TNode> Next;
public TNode Item;
public override string ToString()
{
return string.Format("{0}", Item);
}
}
这样,实现 Push 操作就是用新压入的节点作为新的链表头部,而实现 Pop 操作则是将链表头部取出后将所指向的下一个节点作为新的链表头。
public class SimpleStack<T>
{
private class Node<TNode>
{
public Node<TNode> Next;
public TNode Item;
public override string ToString()
{
return string.Format("{0}", Item);
}
} private Node<T> _head; public SimpleStack()
{
_head = new Node<T>();
} public void Push(T item)
{
Node<T> node = new Node<T>();
node.Item = item; node.Next = _head.Next;
_head.Next = node;
} public T Pop()
{
Node<T> node = _head.Next;
if (node == null)
return default(T); _head.Next = node.Next; return node.Item;
}
}
使用如下代码,先 Push 入栈 1000 个元素,然后在多线程中 Pop 元素。
class Program
{
static void Main(string[] args)
{
SimpleStack<int> stack = new SimpleStack<int>(); for (int i = ; i <= ; i++)
{
stack.Push(i);
} bool[] poppedItems = new bool[]; Parallel.For(, , (i) =>
{
int item = stack.Pop();
if (poppedItems[item])
{
Console.WriteLine(
"Thread [{0:00}] : Item [{1:0000}] was popped before!",
Thread.CurrentThread.ManagedThreadId, item);
}
poppedItems[item] = true;
}); Console.WriteLine("Done.");
Console.ReadLine();
}
}
运行效果如下图所示。

由上图运行结果可知,当多个线程同时 Pop 数据时,可能发生看起来像同一个数据项 Item 被 Pop 出两次的现象。
实现普通的加锁的栈 SimpleLockedStack 类
那么为了保持一致性和准确性,首先想到的办法就是加锁。lock 不仅可以保护代码区域内的指令不会被重排,还能在获取锁之后阻止其他线程修改数据。
public class SimpleLockedStack<T>
{
private class Node<TNode>
{
public Node<TNode> Next;
public TNode Item;
public override string ToString()
{
return string.Format("{0}", Item);
}
} private Node<T> _head;
private object _sync = new object(); public SimpleLockedStack()
{
_head = new Node<T>();
} public void Push(T item)
{
lock (_sync)
{
Node<T> node = new Node<T>();
node.Item = item; node.Next = _head.Next;
_head.Next = node;
}
} public T Pop()
{
lock (_sync)
{
Node<T> node = _head.Next;
if (node == null)
return default(T); _head.Next = node.Next; return node.Item;
}
}
}
加锁之后,显然运行结果就不会出错了。

实现 Lock-Free 的栈 LockFreeStack 类
但显然我们更关注性能问题,当有多个线程在交错 Push 和 Pop 操作时,
- 首先我们不希望发生等待锁现象,如果线程取得锁后被更高优先级的操作调度抢占,则所有等待锁的线程都被阻塞;
- 其次我们不希望线程等待锁的时间过长;
所以准备采用 Lock-Free 技术,通过引入 CAS 操作通过细粒度锁来实现。此处 CAS 使用 C# 中的 Interlocked.CompareExchange 方法,该操作是原子的,并且有很多重载方法可供使用。
private static bool CAS(
ref Node<T> location, Node<T> newValue, Node<T> comparand)
{
return comparand ==
Interlocked.CompareExchange<Node<T>>(
ref location, newValue, comparand);
}
在实现 CAS Loops 时,我们使用 do..while.. 语法来完成。
public void Push(T item)
{
Node<T> node = new Node<T>();
node.Item = item; do
{
node.Next = _head.Next;
}
while (!CAS(ref _head.Next, node, node.Next));
}
这样,新的 LockFreeStack 类就诞生了。
public class LockFreeStack<T>
{
private class Node<TNode>
{
public Node<TNode> Next;
public TNode Item;
public override string ToString()
{
return string.Format("{0}", Item);
}
} private Node<T> _head; public LockFreeStack()
{
_head = new Node<T>();
} public void Push(T item)
{
Node<T> node = new Node<T>();
node.Item = item; do
{
node.Next = _head.Next;
}
while (!CAS(ref _head.Next, node, node.Next));
} public T Pop()
{
Node<T> node; do
{
node = _head.Next; if (node == null)
return default(T);
}
while (!CAS(ref _head.Next, node.Next, node)); return node.Item;
} private static bool CAS(
ref Node<T> location, Node<T> newValue, Node<T> comparand)
{
return comparand ==
Interlocked.CompareExchange<Node<T>>(
ref location, newValue, comparand);
}
}
这个新的类的测试结果正如我们想象也是正确的。

实现 ConcurrentStack 类
那实现 LockFreeStack 类之后,实际已经满足了 Lock-Free 的条件要求,我们还能不能做的更好呢?
我们来观察下上面实现的 Push 方法:
public void Push(T item)
{
Node<T> node = new Node<T>();
node.Item = item; do
{
node.Next = _head.Next;
}
while (!CAS(ref _head.Next, node, node.Next));
}
发现当 CAS 操作判定失败时,立即进入下一次循环判定。而在实践中,当 CAS 判定失败时,是因为其他线程正在更改相同的内存数据,如果立即再进行 CAS 判定则失败几率会更高,我们需要给那些正在修改数据的线程时间以完成操作,所以这里当前线程最好能 "休息" 一会。
"休息" 操作我们选用 .NET 中提供的轻量级(4 Bytes)线程等待机制 SpinWait 类。
public void Push(T item)
{
Node<T> node = new Node<T>();
node.Item = item; SpinWait spin = new SpinWait(); while (true)
{
Node<T> oldHead = _head;
node.Next = oldHead.Next; if (Interlocked.CompareExchange(ref _head, node, oldHead) == oldHead)
break; spin.SpinOnce();
}
}
实际上 SpinOnce() 方法调用了 Thread.SpinWait() 等若干操作,那么这些操作到底做了什么并且耗时多久呢?
首先,Thread.SpinWait(N) 会在当前 CPU 上紧凑的循环 N 个周期,每个周期都会发送 PAUSE 指令给 CPU,告诉 CPU 当前正在执行等待,不要做其他工作了。所以,重点是 N 的值是多少。在 .NET 中实现的 SpinOne 中根据统计意义的度量,将此处的 N 根据调用次数来变化。
- 第一次调用,N = 4;
- 第二次调用,N = 8;
- ...
- 第十次调用,N = 2048;
那么在 10 次调用之后呢?
10 次之后 SpinOnce 就不再进行 Spin 操作了,它根据情况选择进入不同的 Yield 流程。
- Thread.Yield:调用静态方法 Thread.Yield(),如果在相同的 CPU Core 上存在相同或较低优先级的线程正在等待执行,则当前线程让出时间片。如果没有找到这样的线程,则当前线程继续运行。
- Thread.Sleep(0):将 0 传递给 Thread.Sleep(),产生的行为与 Thread.Yield() 类似,唯一的区别就是要在所有的 CPU Core 上查找的相同或较低优先级的线程,而不仅限于当前的 CPU Core。如果没有找到这样的线程,则当前线程继续运行。
- Thread.Sleep(1):当前线程此时真正的进入了睡眠状态(Sleep State)。虽然指定的是 1 毫秒,但依据不同系统的时间精度不同,这个操作可能花费 10-15 毫秒。
上面三种情况在 SpinOnce 中是根据如下的代码来判断执行的。
int yieldsSoFar = (m_count >= ? m_count - : m_count); if ((yieldsSoFar % ) == ( - ))
{
Thread.Sleep();
}
else if ((yieldsSoFar % ) == ( - ))
{
Thread.Sleep();
}
else
{
Thread.Yield();
}
这样,我们就可以通过添加失败等待来进一步优化,形成了新的 ConcurrentStack 类。
// A stack that uses CAS operations internally to maintain
// thread-safety in a lock-free manner. Attempting to push
// or pop concurrently from the stack will not trigger waiting,
// although some optimistic concurrency and retry is used,
// possibly leading to lack of fairness and/or live-lock.
// The stack uses spinning and back-off to add some randomization,
// in hopes of statistically decreasing the possibility of live-lock.
//
// Note that we currently allocate a new node on every push.
// This avoids having to worry about potential ABA issues,
// since the CLR GC ensures that a memory address cannot be
// reused before all references to it have died. /// <summary>
/// Represents a thread-safe last-in, first-out collection of objects.
/// </summary>
public class ConcurrentStack<T>
{
// A volatile field should not normally be passed using a ref or out parameter,
// since it will not be treated as volatile within the scope of the function.
// There are exceptions to this, such as when calling an interlocked API.
// As with any warning, you may use the #pragma warning to disable this warning
// in those rare cases where you are intentionally using a volatile field
// as a reference parameter.
#pragma warning disable 420 /// <summary>
/// A simple (internal) node type used to store elements
/// of concurrent stacks and queues.
/// </summary>
private class Node
{
internal readonly T m_value; // Value of the node.
internal Node m_next; // Next pointer. /// <summary>
/// Constructs a new node with the specified value and no next node.
/// </summary>
/// <param name="value">The value of the node.</param>
internal Node(T value)
{
m_value = value;
m_next = null;
}
} // The stack is a singly linked list, and only remembers the head.
private volatile Node m_head; /// <summary>
/// Inserts an object at the top of the stack.
/// </summary>
/// <param name="item">The object to push onto the stack.
/// The value can be a null reference for reference types.
/// </param>
public void Push(T item)
{
// Pushes a node onto the front of the stack thread-safely.
// Internally, this simply swaps the current head pointer
// using a (thread safe) CAS operation to accomplish lock freedom.
// If the CAS fails, we add some back off to statistically
// decrease contention at the head, and then go back around and retry. Node newNode = new Node(item);
newNode.m_next = m_head;
if (Interlocked.CompareExchange(
ref m_head, newNode, newNode.m_next) == newNode.m_next)
{
return;
} // If we failed, go to the slow path and loop around until we succeed.
SpinWait spin = new SpinWait(); // Keep trying to CAS the existing head with
// the new node until we succeed.
do
{
spin.SpinOnce();
// Reread the head and link our new node.
newNode.m_next = m_head;
}
while (Interlocked.CompareExchange(
ref m_head, newNode, newNode.m_next) != newNode.m_next);
} /// <summary>
/// Attempts to pop and return the object at the top of the stack.
/// </summary>
/// <param name="result">
/// When this method returns, if the operation was successful,
/// result contains the object removed.
/// If no object was available to be removed, the value is unspecified.
/// </param>
/// <returns>true if an element was removed and returned
/// from the top of the stack successfully; otherwise, false.</returns>
public bool TryPop(out T result)
{
// Capture the original value from memory
Node head = m_head; // Is the stack empty?
if (head == null)
{
result = default(T);
return false;
} if (Interlocked.CompareExchange(
ref m_head, head.m_next, head) == head)
{
result = head.m_value;
return true;
} // Fall through to the slow path.
SpinWait spin = new SpinWait(); // Try to CAS the head with its current next.
// We stop when we succeed or when we notice that
// the stack is empty, whichever comes first.
int backoff = ; // avoid the case where TickCount could return Int32.MinValue
Random r = new Random(Environment.TickCount & Int32.MaxValue); while (true)
{
// Capture the original value from memory
head = m_head; // Is the stack empty?
if (head == null)
{
result = default(T);
return false;
} // Try to swap the new head. If we succeed, break out of the loop.
if (Interlocked.CompareExchange(
ref m_head, head.m_next, head) == head)
{
result = head.m_value;
return true;
} // We failed to CAS the new head. Spin briefly and retry.
for (int i = ; i < backoff; i++)
{
spin.SpinOnce();
} // Arbitrary number to cap back-off.
backoff = spin.NextSpinWillYield ? r.Next(, ) : backoff * ;
}
} /// <summary>
/// Gets a value that indicates whether the stack is empty.
/// </summary>
/// <value>true if the stack is empty; otherwise, false.</value>
public bool IsEmpty
{
// Checks whether the stack is empty. Clearly the answer
// may be out of date even prior to
// the function returning (i.e. if another thread
// concurrently adds to the stack). It does
// guarantee, however, that, if another thread
// does not mutate the stack, a subsequent call
// to TryPop will return true
// -- i.e. it will also read the stack as non-empty.
get { return m_head == null; }
} /// <summary>
/// Gets the number of elements contained in the stack.
/// </summary>
/// <value>The number of elements contained in the stack.</value>
public int Count
{
// Counts the number of entries in the stack.
// This is an O(n) operation. The answer may be out of date before
// returning, but guarantees to return a count that was once valid.
// Conceptually, the implementation snaps a copy of the list and
// then counts the entries, though physically this is not
// what actually happens.
get
{
int count = ; // Just whip through the list and tally up the number of nodes.
// We rely on the fact that
// node next pointers are immutable after being en-queued
// for the first time, even as
// they are being dequeued. If we ever changed this
// (e.g. to pool nodes somehow),
// we'd need to revisit this implementation. for (Node curr = m_head; curr != null; curr = curr.m_next)
{
// we don't handle overflow, to be consistent with existing
// generic collection types in CLR
count++;
} return count;
}
} /// <summary>
/// Removes all objects from the this stack.
/// </summary>
public void Clear()
{
// Clear the list by setting the head to null.
// We don't need to use an atomic operation for this:
// anybody who is mutating the head by pushing or popping
// will need to use an atomic operation to guarantee they
// serialize and don't overwrite our setting of the head to null.
m_head = null;
} #pragma warning restore 420
}
实际上,上面的 ConcurrentStack<T> 类就是 .NET Framework 中 System.Collections.Concurrent.ConcurrentStack<T> 类的基本实现过程。
参考资料
- Non-blocking algorithm
- ConcurrentStack Source Code
- C# Interlocked functions as a lock mechanism?
- InterlockedCompareExchangePointer Intrinsic Functions
- Improve efficiency and fairness when combining temporally close events
- SpinWait and Lock-Free code
- LOCK-FREE DATA STRUCTURES: THE STACK
- THE ABA PROBLEM
- Interlocked operations don't solve everything
- Volatile keyword in C# – memory model explained
- What Interlocked.CompareExchange is used for in the dapper .net method?
- Benefits, drawbacks of lock-free programming for multicore
- Understand the Impact of Low-Lock Techniques in Multithreaded Apps
- Concurrency : What Every Dev Must Know About Multithreaded Apps
- Designing Applications for High Performance - Part II
- Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2A
- x86 Instruction Set Reference - CMPXCHG
- An Introduction to Lock-Free Programming
- Lock-Free Programming
- Understanding Atomic Operations
- Introduction to Lock-free Programming with C++ and Qt
- ECMA C# and Common Language Infrastructure Standards
- Lock-Free Concurrent Data Structures, CAS and the ABA-Problem
- 透过 Linux 内核看无锁编程
- 无锁队列的实现
- Implementing Lock-Free Queues
- 用于并行计算的多线程数据结构,第 2 部分: 设计不使用互斥锁的并发数据结构
- Yet another implementation of a lock-free circular array queue
- Writing Lock-Free Code: A Corrected Queue
本文《Lock Free 编程》由作者 Dennis Gao 发表自博客园博客,任何未经作者本人允许的人为或爬虫转载均为耍流氓。
Lock-Free 编程的更多相关文章
- 并发编程 17—— Lock
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 01—— ThreadLocal
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 20—— AbstractQueuedSynchronizer 深入分析
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 02—— ConcurrentHashMap
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 04——闭锁CountDownLatch 与 栅栏CyclicBarrier
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 05—— Callable和Future
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 06—— CompletionService :Executor 和 BlockingQueue
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 10—— 任务取消 之 关闭 ExecutorService
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 并发编程 12—— 任务取消与关闭 之 shutdownNow 的局限性
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
随机推荐
- Java多线程简析
一.线程的状态: 线程共有下面4种状态: 1.新建状态(New): 新创建了一个线程对象,当你用new创建一个线程时,该线程尚未运行. 2.就绪状态(Runnable): 线程对象创建后,其他线程调用 ...
- python安装使用talib
安装主要在http://www.lfd.uci.edu/~gohlke/pythonlibs/这个网站找到 按照需要的python版本和平台位数下载,然后直接用pip install 进行安装 包含的 ...
- 【转】HTTP POST GET 本质区别详解
一 原理区别 一般在浏览器中输入网址访问资源都是通过GET方式:在FORM提交中,可以通过Method指定提交方式为GET或者POST,默认为GET提交 Http定义了与服务器交互的不同方法,最基本的 ...
- ubuntu如何傻瓜式安装eric6
最近在搞PyQt5,听闻eric6是一个不错的IDE,但就是配置起来略蛋疼. 在网上搜到不少教程,都是要先编译安装Qt5, PyQt5, sip, qscintilla2, qscintilla2又分 ...
- margin:0 auto 与 text-align:center 的区别
基本概念: 1.text-align: 属性规定元素中的文本的水平对齐方式; 该属性通过指定行框与哪个点对齐,从而设置块级元素内文本的水平对齐方式; 一般情况下设置文本对齐方式的时使用此属性.支 ...
- nio 弊端
java-nio在Android上使用的种种弊端 August 12, 2013programming 我们知道,手机上的网络一般会比较慢(使用wifi除外),用户非常不希望自己在使用手机时被考验耐心 ...
- Being a Good Boy in Spring Festival 尼姆博弈
Time Limit:1000MS Memory Limit:32768KB 64bit IO Format:%I64d & %I64u Submit Status Descr ...
- 【OpenGL】如何绘制Shadow
背景 Shadow即阴影,它是光线被不透明物体遮挡而产生的黑暗区域,与光源的方向相反. 在Blender中编辑过程中没有Shadow,只有在经过渲染后才能显示.目前有一个基于Blender的项目,要求 ...
- jQuery学习-css、class操作、动画方法的运用、jQ操作Dom节点
css操作(设置单个/多个样式.获取样式) //修改单个属性:括号之中直接是需要修改的样式名,值 css(name,value) //例:$("#one").css("b ...
- java继承的构造函数
1.在构造函数的继承中,子类不能继承父类的构造函数.但是子类却可以用super()或super(参数)使用父类的构造函数, <1>无参的构造函数: //父类 public class An ...