<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Queue | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/tag/queue/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Mon, 06 Jul 2020 10:11:27 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>一个“蝇量级” C 语言协程库</title>
		<link>https://coolshell.cn/articles/10975.html</link>
					<comments>https://coolshell.cn/articles/10975.html#comments</comments>
		
		<dc:creator><![CDATA[Leo]]></dc:creator>
		<pubDate>Tue, 28 Jan 2014 02:50:41 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[趣味问题]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[coroutine]]></category>
		<category><![CDATA[Queue]]></category>
		<category><![CDATA[yield]]></category>
		<category><![CDATA[协程]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10975</guid>

					<description><![CDATA[<p>（感谢网友 @我的上铺叫路遥 投稿） 协程(coroutine)顾名思义就是“协作的例程”（co-operative routines）。跟具有操作系统概念的线...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10975.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10975.html">一个“蝇量级” C 语言协程库</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 </strong><a href="http://weibo.com/fullofbull" target="_blank"><strong>@我的上铺叫路遥</strong></a><strong> 投稿）</strong></p>
<p>协程(coroutine)顾名思义就是“协作的例程”（co-operative routines）。跟具有操作系统概念的线程不一样，协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。实际上协程的概念比线程还要早，按照 Knuth 的说法<strong>“子例程是协程的特例”</strong>，一个子例程就是一次子函数调用，那么实际上协程就是类函数一样的程序组件，你可以在一个线程里面轻松创建数十万个协程，就像数十万次函数调用一样。只不过子例程只有一个调用入口起始点，返回之后就结束了，而协程入口既可以是起始点，又可以从上一个返回点继续执行，也就是说协程之间可以通过 yield 方式转移执行权，<strong>对称（symmetric）、平级</strong>地调用对方，而不是像例程那样上下级调用关系。当然 Knuth 的“特例”指的是协程也可以模拟例程那样实现上下级调用关系，这就叫<strong>非对称协程</strong>（asymmetric coroutines）。</p>
<h4>基于事件驱动模型</h4>
<p>我们举一个例子来看看一种<strong>对称协程</strong>调用场景，大家最熟悉的“生产者-消费者”事件驱动模型，一个协程负责生产产品并将它们加入队列，另一个负责从队列中取出产品并使用它。为了提高效率，你想一次增加或删除多个产品。伪代码可以是这样的：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># producer coroutine
loop
while queue is not full
  create some new items
  add the items to queue
yield to consumer

# consumer coroutine
loop
while queue is not empty
  remove some items from queue
  use the items
yield to producer</pre>
<p><span id="more-10975"></span></p>
<p>大多数教材上拿这种模型作为多线程的例子，实际上多线程在此的应用还是显得有点“重量级”，由于缺乏 yield 语义，线程之间不得不使用同步机制来避免产生全局资源的竟态，这就不可避免产生了休眠、调度、切换上下文一类的系统开销，而且线程调度还会产生时序上的不确定性。而对于协程来说，“挂起”的概念只不过是转让代码执行权并调用另外的协程，待到转让的协程告一段落后重新得到调用并从挂起点“唤醒”，这种协程间的调用是逻辑上可控的，时序上确定的，可谓一切尽在掌握中。</p>
<p>当今一些具备协程语义的语言，比较重量级的如C#、erlang、golang，以及轻量级的python、lua、javascript、ruby，还有函数式的scala、scheme等。相比之下，作为原生态语言的 C 反而处于尴尬的地位，原因在于 C 依赖于一种叫做<strong>栈帧</strong>的例程调用，例程内部的状态量和返回值都保留在堆栈上，这意味着生产者和消费者相互之间无法实现平级调用，当然你可以改写成把生产者作为主例程然后将产品作为传递参数调用消费者例程，这样的代码写起来费力不讨好而且看起来会很难受，特别当协程数目达到十万数量级，这种写法就过于僵化了。</p>
<p>这就引出了协程的概念，<strong>如果将每个协程的上下文（比如程序计数器）保存在其它地方而不是堆栈上，协程之间相互调用时，被调用的协程只要从堆栈以外的地方恢复上次出让点之前的上下文即可，这有点类似于 CPU 的上下文切换，</strong>遗憾的是似乎只有更底层的汇编语言才能做到这一点。</p>
<p>难道 C 语言只能用多线程吗？幸运的是，C 标准库给我们提供了两种协程调度原语：一种是<a title="http://zh.wikipedia.org/wiki/Setjmp.h" href="http://zh.wikipedia.org/wiki/Setjmp.h" target="_blank"> setjmp/longjmp</a>，另一种是<a title="http://pubs.opengroup.org/onlinepubs/7990989799/xsh/ucontext.h.html" href="http://pubs.opengroup.org/onlinepubs/7990989799/xsh/ucontext.h.html" target="_blank"> ucontext 组件</a>，它们内部（当然是用汇编语言）实现了协程的上下文切换，相较之下前者在应用上会产生相当的不确定性（比如不好封装，具体说明参考联机文档），所以后者应用更广泛一些，网上绝大多数 C 协程库也是基于 ucontext 组件实现的。</p>
<h4>“蝇量级”的协程库</h4>
<p>在此，我来介绍一种“蝇量级”的开源 C 协程库 <a title="http://dunkels.com/adam/pt/" href="http://dunkels.com/adam/pt/" target="_blank">protothreads</a>。这是一个全部用 ANSI C 写成的库，之所以称为“蝇量级”的，就是说，实现已经不能再精简了，几乎就是原语级别。事实上 protothreads 整个库不需要链接加载，因为所有源码都是头文件，类似于 STL 这样不依赖任何第三方库，在任何平台上可移植；总共也就 5 个头文件，有效代码量不足 100 行；API 都是宏定义的，所以不存在调用开销；最后，每个协程的空间开销是 2 个字节（是的，你没有看错，就是一个 short 单位的“栈”！）当然这种精简是要以使用上的局限为代价的，接下来的分析会说明这一点。</p>
<p>先来看看 protothreads 作者，<a title="http://dunkels.com/adam/" href="http://dunkels.com/adam/" target="_blank">Adam Dunkels</a>，一位来自瑞典皇家理工学院的计算机天才帅哥。话说这哥们挺有意思的，写了好多轻量级的作品，都是 BSD 许可证。顺便说一句，轻量级开源软件全世界多如牛毛，可像这位哥们写得如此出名的并不多。比如嵌入式网络操作系统 <a title="http://www.contiki-os.org/" href="http://www.contiki-os.org/" target="_blank">Contiki</a>，国人耳熟能详的 TCP/IP 协议栈 <a title="http://en.wikipedia.org/wiki/UIP_(micro_IP)" href="http://en.wikipedia.org/wiki/UIP_(micro_IP)" target="_blank">uIP</a> 和 <a title="http://savannah.nongnu.org/projects/lwip/" href="http://savannah.nongnu.org/projects/lwip/" target="_blank">lwIP</a> 也是出自其手。上述这些软件都是经过数十年企业级应用的考验，质量之高可想而知。</p>
<p>很多人会好奇如此“蝇量级”的代码究竟是怎么实现的呢？在分析 protothreads 源码之前，我先来给大家补一补 C 语言的基础课;-^)简而言之，这利用了 C 语言特性上的一个“奇技淫巧”，而且这种技巧恐怕连许多具备十年以上经验的 C 程序员老手都不见得知晓。当然这里先要声明我不是推荐大家都这么用，实际上这是以破坏语言的代码规范为代价，在一些严肃的项目工程中需要谨慎对待，除非你想被炒鱿鱼。</p>
<h4>C 语言的“yield 语义”</h4>
<p>下面的教程来自于一位 ARM 工程师、天才黑客 <a title="http://www.chiark.greenend.org.uk/~sgtatham/" href="http://www.chiark.greenend.org.uk/~sgtatham/" target="_blank">Simon Tatham</a>（开源 Telnet/SSH 客户端 <a title="http://www.chiark.greenend.org.uk/~sgtatham/putty/" href="http://www.chiark.greenend.org.uk/~sgtatham/putty/" target="_blank">PuTTY</a> 和汇编器 <a title="http://www.nasm.us/" href="http://www.nasm.us/" target="_blank">NASM</a> 的作者，吐槽一句，PuTTY的源码号称是所有正式项目里最难 hack 的 C，你应该猜到作者是什么语言出身）的博文：<a title="http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html" href="http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html" target="_blank">Coroutines in C</a>。中文译文在<a title="http://www.oschina.net/translate/coroutines-in-c" href="http://www.oschina.net/translate/coroutines-in-c" target="_blank">这里</a>。</p>
<p>我们知道 python 的 yield 语义功能类似于一种迭代生成器，函数会保留上次的调用状态，并在下次调用时会从上个返回点继续执行。用 C 语言来写就像这样：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  int i;
  for (i = 0; i &lt; 10; i++)
    return i;   /* won&#039;t work, but wouldn&#039;t it be nice */
}</pre>
<p>连续对它调用 10 次，它能分别返回 0 到 9。该怎样实现呢？可以利用 goto 语句，如果我们在函数中加入一个状态变量，就可以这样实现：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: goto LABEL0;
    case 1: goto LABEL1;
  }
  LABEL0: /* start of function */
  for (i = 0; i &lt; 10; i++) {
    state = 1; /* so we will come back to LABEL1 */
    return i;
    LABEL1:; /* resume control straight after the return */
  }
}</pre>
<p>这个方法是可行的。我们在所有需要 yield 的位置都加上标签：起始位置加一个，还有所有 return 语句之后都加一个。每个标签用数字编号，我们在状态变量中保存这个编号，这样就能在我们下次调用时告诉我们应该跳到哪个标签上。每次返回前，更新状态变量，指向到正确的标签；不论调用多少次，针对状态变量的 switch 语句都能找到我们要跳转到的位置。</p>
<p>但这还是难看得很。最糟糕的部分是所有的标签都需要手工维护，还必须保证函数中的标签和开头 switch 语句中的一致。每次新增一个 return 语句，就必须想一个新的标签名并将其加到 switch 语句中；每次删除 return 语句时，同样也必须删除对应的标签。这使得维护代码的工作量增加了一倍。</p>
<p>仔细想想，其实我们可以不用 switch 语句来决定要跳转到哪里去执行，而是<strong>直接利用 switch 语句本身来实现跳转</strong>：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: /* start of function */
    for (i = 0; i &lt; 10; i++) {
      state = 1; /* so we will come back to &quot;case 1&quot; */
      return i;
      case 1:; /* resume control straight after the return */
    }
  }
}</pre>
<p>酷！没想到 switch-case 语句可以这样用，其实说白了 C 语言就是脱胎于汇编语言的，switch-case 跟 if-else 一样，无非就是汇编的条件跳转指令的另类实现而已（这也间接解释了为何汇编程序员经常揶揄 C 语言是“大便一样的代码”）。我们还可以用 __LINE__ 宏使其更加一般化：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: /* start of function */
    for (i = 0; i &lt; 10; i++) {
      state = __LINE__ + 2; /* so we will come back to &quot;case __LINE__&quot; */
      return i;
      case __LINE__:; /* resume control straight after the return */
    }
  }
}</pre>
<p>这样一来我们可以用宏提炼出一种范式，封装成组件：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#define Begin() static int state=0; switch(state) { case 0:
#define Yield(x) do { state=__LINE__; return x; case __LINE__:; } while (0)
#define End() }
int function(void) {
  static int i;
  Begin();
  for (i = 0; i &lt; 10; i++)
    Yield(i);
  End();
}</pre>
<p>怎么样，看起来像不像发明了一种全新的语言？<strong>实际上我们利用了 switch-case 的分支跳转特性，以及预编译的 __LINE__ 宏，实现了一种隐式状态机，最终实现了“yield 语义”。</strong></p>
<p>还有一个问题，当你欢天喜地地将这种鲜为人知的技巧运用到你的项目中，并成功地拿去向你的上司邀功问赏的时候，你的上司会怎样看待你的代码呢？你的宏定义中大括号没有匹配完整，在代码块中包含了未用到的 case，Begin 和 Yield 宏里面不完整的七拼八凑……你简直就是公司里不遵守编码规范的反面榜样！</p>
<p>别着急，在原文中 Simon Tatham 大牛帮你找到一个坚定的反驳理由，我觉得对程序员来说简直是金玉良言。</p>
<p>将编程规范用在这里是不对的。文章里给出的示例代码不是很长，也不很复杂，即便以状态机的方式改写还是能够看懂的。但是随着代码越来越长，改写的难度将越来越大，改写对直观性造成的损失也变得相当相当大。</p>
<p>想一想，一个函数如果包含这样的小代码块：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">case STATE1:
/* perform some activity */
if (condition) state = STATE2; else state = STATE3;</pre>
<p>对于看代码的人说，这和包含下面小代码块的函数没有多大区别：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">LABEL1:
/* perform some activity */
if (condition) goto LABEL2; else goto LABEL3;</pre>
<p>是的，这两个函数的结构在视觉上是一样的，而对于函数中实现的算法，两个函数都一样不利于查看。因为你使用协程的宏而炒你鱿鱼的人，一样会因为你写的函数是由小块的代码和 goto 语句组成而吼着炒了你。只是这次他们没有冤枉你，因为像那样设计的函数会严重扰乱算法的结构。</p>
<p><strong>编程规范的目标就是为了代码清晰。</strong>如果将一些重要的东西，像 switch、return 以及 case 语句，隐藏到起“障眼”作用的宏中，从编程规范的角度讲，可以说你扰乱了程序的语法结构，并且违背了代码清晰这一要求。但是我们这样做是为了突出程序的算法结构，而算法结构恰恰是看代码的人更想了解的。</p>
<p><span style="color: #ff0000;"><strong>任何编程规范，坚持牺牲算法清晰度来换取语法清晰度的，都应该重写。</strong></span>如果你的上司因为使用了这一技巧而解雇你，那么在保安把你往外拖的时候要不断告诉他这一点。</p>
<p>原文作者最后给出了一个 MIT 许可证的 <a title="http://www.chiark.greenend.org.uk/~sgtatham/coroutine.h" href="http://www.chiark.greenend.org.uk/~sgtatham/coroutine.h" target="_blank">coroutine.h</a> 头文件。值得一提的是，正如文中所说，这种协程实现方法有个使用上的局限，就是<strong>协程调度状态的保存依赖于 static 变量，而不是堆栈上的局部变量</strong>，实际上也无法用局部变量（堆栈）来保存状态，这就使得代码不具备可重入性和多线程应用。后来作者补充了一种技巧，就是将局部变量包装成函数参数传入的一个虚构的上下文结构体指针，然后用动态分配的堆来“模拟”堆栈，解决了线程可重入问题。但这样一来反而有损代码清晰，比如所有局部变量都要写成对象成员的引用方式，特别是局部变量很多的时候很麻烦，再比如宏定义 malloc/free 的玩法过于托大，不易控制，搞不好还增加了被炒鱿鱼的风险（只不过这次是你活该）。</p>
<p>我个人认为，既然协程本身是一种单线程的方案，那么我们应该假定应用环境是单线程的，不存在代码重入问题，所以我们可以大胆地使用 static 变量，维持代码的简洁和可读性。事实上<strong>我们也不应该在多线程环境下考虑使用这么简陋的协程</strong>，非要用的话，前面提到 glibc 的 ucontext 组件也是一种可行的替代方案，它提供了一种协程私有堆栈的上下文，当然这种用法在跨线程上也并非没有限制，请仔细阅读联机文档。</p>
<h4>Protothreads的上下文</h4>
<p>感谢 Simon Tatham 的淳淳教诲，接下来我们可以 hack 一下源码了。先来看看实现 protothreads 的数据结构， 实际上它就是协程的<strong>上下文结构体</strong>，用以保存状态变量，相信你很快就明白为何它的“堆栈”只有 2 个字节：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct pt {
  lc_t lc;
}</pre>
<p>里面只有一个 short 类型的变量，实际上它是用来保存上一次出让点的程序计数器。这也映证了协程比线程的灵活之处，就是协程可以是 stackless 的，如果需要实现的功能很单一，比如像生产者-消费者模型那样用来做事件通知，那么实际上协程需要保存的状态变量仅仅是一个程序计数器即可。像 python generator 也是 stackless 的，当然实现一个迭代生成器可能还需要保留上一个迭代值，前面 C 的例子是用 static 变量保存，你也可以设置成员变量添加到上下文结构体里面。如果你真的不确定用协程调度时需要保存多少状态变量，那还是用 ucontext 好了，它的上下文提供了堆栈和信号，但是由用户负责分配资源，详细使用方法见联机文档。。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">typedef struct ucontext {
  struct ucontext_t *uc_link;
  sigset_t uc_sigmask;
  stack_t uc_stack;
  ...
} ucontext_t;</pre>
<h4>Protothreads的原语和组件</h4>
<p>有点扯远了，回到 protothreads，看看提供的协程“原语”。有两种实现方法，在 ANSI C 下，就是传统的 switch-case 语句：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#define LC_INIT（s） s = 0;  // 源码中是有分号的，一个低级 bug，啊哈～
#define LC_RESUME(s) switch (s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) }
</pre>
<p>但这种“原语”有个难以察觉的缺陷：<strong>就是你无法在 LC_RESUME 和 LC_END （或者包含它们的组件）之间的代码中使用 switch-case语句，因为这会引起外围的 switch 跳转错误！</strong>为此，protothreads 又实现了基于 GNU C 的调度“原语”。在 GNU C 下还有一种语法糖叫做标签指针，就是在一个 label 前面加 &amp;&amp;（不是地址的地址，是 GNU 自定义的符号），可以用 void 指针类型保存，然后 goto 跳转：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">typedef void * lc_t；
#define LC_INIT(s) s = NULL
#define LC_RESUME(s) \
  do { \
    if (s != NULL) { \
      goto *s; \
    }
  } while (0)
#define LC_CONCAT2(s1, s2) s1##s2
#define LC_CONCAT(s1, s2) LC_CONCAT2(s1, s2)
#define LC_SET(s) \
  do { \
    LC_CONCAT(LC_LABEL, __LINE__): \
    （s） = &amp;&amp;LC_CONCAT(LC_LABEL, __LINE__); \
  } while (0)</pre>
<p>好了，有了前面的基础知识，理解这些“原语”就是小菜一叠，下面看看如何建立“组件”，同时也是 protothreads API，我们先定义四个退出码作为协程的<strong>调度状态机</strong>：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED  2
#define PT_ENDED   3</pre>
<p>下面这些 API 可直接在应用程序中调用：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">/* 初始化一个协程，也即初始化状态变量 */
#define PT_INIT(pt) LC_INIT((pt)-&gt;lc)

/* 声明一个函数，返回值为 char 即退出码，表示函数体内使用了 proto thread，（个人觉得有些多此一举） */
#define PT_THREAD(name_args) char name_args

/* 协程入口点， PT_YIELD_FLAG=0表示出让，=1表示不出让，放在 switch 语句前面，下次调用的时候可以跳转到上次出让点继续执行 */
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)-&gt;lc)

/* 协程退出点，至此一个协程算是终止了，清空所有上下文和标志 */
#define PT_END(pt) LC_END((pt)-&gt;lc); PT_YIELD_FLAG = 0; \
                   PT_INIT(pt); return PT_ENDED; }

/* 协程出让点，如果此时协程状态变量 lc 已经变为 __LINE__ 跳转过来的，那么 PT_YIELD_FLAG = 1，表示从出让点继续执行。 */
#define PT_YIELD(pt)        \
  do {            \
    PT_YIELD_FLAG = 0;        \
    LC_SET((pt)-&gt;lc);       \
    if(PT_YIELD_FLAG == 0) {      \
      return PT_YIELDED;      \
    }           \
  } while(0)

/* 附加出让条件 */
#define PT_YIELD_UNTIL(pt, cond)    \
  do {            \
    PT_YIELD_FLAG = 0;        \
    LC_SET((pt)-&gt;lc);       \
    if((PT_YIELD_FLAG == 0) || !(cond)) { \
      return PT_YIELDED;      \
    }           \
  } while(0)

/* 协程阻塞点(blocking),本质上等同于 PT_YIELD_UNTIL，只不过退出码是 PT_WAITING，用来模拟信号量同步 */
#define PT_WAIT_UNTIL(pt, condition)          \
  do {            \
    LC_SET((pt)-&gt;lc);       \
    if(!(condition)) {        \
      return PT_WAITING;      \
    }           \
  } while(0)

/* 同 PT_WAIT_UNTIL 条件反转 */
#define PT_WAIT_WHILE(pt, cond)  PT_WAIT_UNTIL((pt), !(cond))

/* 协程调度，调用协程 f 并检查它的退出码，直到协程终止返回 0，否则返回 1。 */
#define PT_SCHEDULE(f) ((f) &lt; PT_EXITED)

/* 这用于非对称协程，调用者是主协程，pt 是和子协程 thread （可以是多个）关联的上下文句柄，主协程阻塞自己调度子协程，直到所有子协程终止 */
#define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread))

/* 用于协程嵌套调度，child 是子协程的上下文句柄 */
#define PT_SPAWN(pt, child, thread)   \
  do {            \
    PT_INIT((child));       \
    PT_WAIT_THREAD((pt), (thread));   \
  } while(0)</pre>
<p>暂时介绍这么多，用户还可以根据自己的需求随意扩展组件，比如实现信号量，你会发现脱离了操作系统环境下的信号量竟是如此简单：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct pt_sem {
  unsigned int count;
};

#define PT_SEM_INIT(s, c) (s)-&gt;count = c

#define PT_SEM_WAIT(pt, s)  \
  do {            \
    PT_WAIT_UNTIL(pt, (s)-&gt;count &gt; 0);    \
    --(s)-&gt;count;       \
  } while(0)

#define PT_SEM_SIGNAL(pt, s) ++(s)-&gt;count</pre>
<p>这些应该不需要我多说了吧，呵呵，让我们回到最初例举的生产者-消费者模型，看看protothreads表现怎样。</p>
<h4>Protothreads实战</h4>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include &quot;pt-sem.h&quot;

#define NUM_ITEMS 32
#define BUFSIZE 8

static struct pt_sem mutex, full, empty;

PT_THREAD(producer(struct pt *pt))
{
  static int produced;

  PT_BEGIN(pt);
  for (produced = 0; produced &lt; NUM_ITEMS; ++produced) {
    PT_SEM_WAIT(pt, &amp;full);
    PT_SEM_WAIT(pt, &amp;mutex);
    add_to_buffer(produce_item());
    PT_SEM_SIGNAL(pt, &amp;mutex);
    PT_SEM_SIGNAL(pt, &amp;empty);
  }
  PT_END(pt);
}

PT_THREAD(consumer(struct pt *pt))
{
  static int consumed;

  PT_BEGIN(pt);
  for (consumed = 0; consumed &lt; NUM_ITEMS; ++consumed) {
    PT_SEM_WAIT(pt, &amp;empty);
    PT_SEM_WAIT(pt, &amp;mutex);
    consume_item(get_from_buffer());
    PT_SEM_SIGNAL(pt, &amp;mutex);
    PT_SEM_SIGNAL(pt, &amp;full);
  }
  PT_END(pt);
}

PT_THREAD(driver_thread(struct pt *pt))
{
  static struct pt pt_producer, pt_consumer;

  PT_BEGIN(pt);
  PT_SEM_INIT(&amp;empty, 0);
  PT_SEM_INIT(&amp;full, BUFSIZE);
  PT_SEM_INIT(&amp;mutex, 1);
  PT_INIT(&amp;pt_producer);
  PT_INIT(&amp;pt_consumer);
  PT_WAIT_THREAD(pt, producer(&amp;pt_producer) &amp; consumer(&amp;pt_consumer));
  PT_END(pt);
}</pre>
<p>源码包中的 example-buffer.c 包含了可运行的完整示例，我就不全部贴了。整体框架就是一个 asymmetric coroutines，包括一个主协程 driver_thread 和两个子协程 producer 和 consumer ，其实不用多说大家也懂的，代码非常清晰直观。我们完全可以通过单线程实现一个简单的事件处理需求，你可以任意添加数十万个协程，几乎不会引起任何额外的系统开销和资源占用。唯一需要留意的地方就是没有一个局部变量，因为 protothreads 是 stackless 的，但这不是问题，首先我们已经假定运行环境是单线程的，其次在一个简化的需求下也用不了多少“局部变量”。如果在协程出让时需要保存一些额外的状态量，像迭代生成器，只要数目和大小都是确定并且可控的话，自行扩展协程上下文结构体即可。</p>
<p>当然这不是说 protothreads 是万能的，它只是贡献了一种模型，你要使用它首先就得学会适应它。下面列举一些 protothreads 的使用限制：</p>
<ul>
<li>由于协程是stackless的，尽量不要使用局部变量，除非该变量对于协程状态是无关紧要的，同理可推，协程所在的代码是不可重入的。</li>
</ul>
<ul>
<li>如果协程使用 switch-case 原语封装的组件，那么禁止在实际应用中使用 switch-case 语句，除非用 GNU C 语法中的标签指针替代。</li>
</ul>
<ul>
<li>一个协程内部可以调用其它例程，比如库函数或系统调用，但必须保证该例程是非阻塞的，否则所在线程内的所有协程都将被阻塞。毕竟线程才是执行的最小单位，协程不过是按“时间片轮度”的例程而已。</li>
</ul>
<p>官网上还例举了更多<a title="http://dunkels.com/adam/pt/examples.html" href="http://dunkels.com/adam/pt/examples.html" target="_blank">实例</a>，都非常实用。另外，一个叫 Craig Graham 的工程师扩展了 pt.h，使得 protothreads 支持 sleep/wake/kill 等操作，文件在此 <a title="http://dunkels.com/adam/download/graham-pt.h" href="http://dunkels.com/adam/download/graham-pt.h" target="_blank">graham-pt.h</a>。</p>
<h4>协程库 DIY 攻略</h4>
<p>看到这里，手养的你是否想迫不及待地 DIY 一个协程组件呢？哪怕很多动态语言本身已经支持了协程语义，很多 C 程序员仍然倾向于自己实现组件，网上很多开源代码底层用的主要还是 glibc 的 ucontext 组件，毕竟提供堆栈的协程组件使用起来更加通用方便。你可以自己写一个调度器，然后模拟线程上下文，再然后……你就能搞出一个跨平台的COS了（笑）。GNU Pth 线程库就是这么实现的，其原作者德国人 <a title="http://engelschall.com/" href="http://engelschall.com/" target="_blank">Ralf S. Engelschall</a> （又是个开源大牛，还写了 <a title="http://engelschall.com/software-artist.php" href="http://engelschall.com/software-artist.php" target="_blank">OpenSSL 等许多作品</a>）就写了一篇<a title="http://xmailserver.org/rse-pmt.pdf" href="http://xmailserver.org/rse-pmt.pdf" target="_blank">论文</a>教大家如何实现一个线程库。另外 protothreads 官网上也有一大堆<a title="http://dunkels.com/adam/pt/links.html" href="http://dunkels.com/adam/pt/links.html" target="_blank">推荐阅读</a>。Have fun！</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/10/edsm-150x150.gif" alt="State Threads 回调终结者" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_title">State Threads 回调终结者</a></li><li ><a href="https://coolshell.cn/articles/8239.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle-150x150.jpg" alt="无锁队列的实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8239.html" class="wp_rp_title">无锁队列的实现</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10975.html">一个“蝇量级” C 语言协程库</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10975.html/feed</wfw:commentRss>
			<slash:comments>54</slash:comments>
		
		
			</item>
		<item>
		<title>无锁队列的实现</title>
		<link>https://coolshell.cn/articles/8239.html</link>
					<comments>https://coolshell.cn/articles/8239.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Fri, 07 Sep 2012 00:26:55 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[Queue]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=8239</guid>

					<description><![CDATA[<p>————注：本文于2019年11月4日更新———— 关于无锁队列的实现，网上有很多文章，虽然本文可能和那些文章有所重复，但是我还是想以我自己的方式把这些文章中的...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/8239.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/8239.html">无锁队列的实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script></p>
<p style="text-align: center;"><strong><em>————注：本文于2019年11月4日更新————</em></strong></p>
<p>关于无锁队列的实现，网上有很多文章，虽然本文可能和那些文章有所重复，但是我还是想以我自己的方式把这些文章中的重要的知识点串起来和大家讲一讲这个技术。下面开始正文。</p>
<h4>关于CAS等原子操作</h4>
<p><img decoding="async" loading="lazy" class="alignright size-full wp-image-8245" title="lock free bicycle" src="https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle.jpg" alt="" width="350" height="261" srcset="https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle.jpg 350w, https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle-300x224.jpg 300w" sizes="(max-width: 350px) 100vw, 350px" />在开始说无锁队列之前，我们需要知道一个很重要的技术就是CAS操作——Compare &amp; Set，或是 Compare &amp; Swap，<strong>现在几乎所有的CPU指令都支持CAS的原子操作，X86下对应的是 <span style="color: #ff0000;">CMPXCHG </span>汇编指令。</strong>有了这个原子操作，我们就可以用其来实现各种无锁（lock free）的数据结构。</p>
<p>这个操作用C语言来描述就是下面这个样子：（代码来自<a href="http://en.wikipedia.org/wiki/Compare-and-swap" target="_blank" rel="noopener noreferrer">Wikipedia的Compare And Swap</a>词条）意思就是说，看一看内存<code>*reg</code>里的值是不是<code>oldval</code>，如果是的话，则对其赋值<code>newval</code>。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">
int compare_and_swap (int* reg, int oldval, int newval)
{
  int old_reg_val = *reg;
  if (old_reg_val == oldval) {
     *reg = newval;
  }
  return old_reg_val;
}
</pre>
<p>我们可以看到，<code>old_reg_val</code> 总是返回，于是，我们可以在 <code>compare_and_swap</code> 操作之后对其进行测试，以查看它是否与 <code>oldval</code>相匹配，因为它可能有所不同，这意味着另一个并发线程已成功地竞争到 <code>compare_and_swap</code> 并成功将 <code>reg</code> 值从 <code>oldval</code> 更改为别的值了。</p>
<p>这个操作可以变种为返回bool值的形式（返回 bool值的好处在于，可以调用者知道有没有更新成功）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">bool compare_and_swap (int *addr, int oldval, int newval)
{
  if ( *addr != oldval ) {
      return false;
  }
  *addr = newval;
  return true;
}</pre>
<p>与CAS相似的还有下面的原子操作：（这些东西大家自己看Wikipedia，也没什么复杂的）</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Fetch-and-add" target="_blank" rel="noopener noreferrer">Fetch And Add</a>，一般用来对变量做 +1 的原子操作</li>
<li><a title="Test-and-set" href="http://en.wikipedia.org/wiki/Test-and-set">Test-and-set</a>，写值到某个内存位置并传回其旧值。汇编指令BST</li>
<li><a title="Test and Test-and-set" href="http://en.wikipedia.org/wiki/Test_and_Test-and-set">Test and Test-and-set</a>，用来低低Test-and-Set的资源争夺情况</li>
</ul>
<p><strong>注：</strong>在实际的C/C++程序中，CAS的各种实现版本如下：</p>
<p><span id="more-8239"></span></p>
<p><strong>1）GCC的CAS</strong></p>
<p style="padding-left: 30px;">GCC4.1+版本中支持CAS的原子操作（完整的原子操作可参看<a href="http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html" target="_blank" rel="noopener noreferrer"> GCC Atomic Builtins</a>）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)</pre>
<p><strong>2）Windows的CAS</strong></p>
<p style="padding-left: 30px;">在Windows下，你可以使用下面的Windows API来完成CAS：（完整的Windows原子操作可参看MSDN的<a href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms686360(v=vs.85).aspx#interlocked_functions" target="_blank" rel="noopener noreferrer">InterLocked Functions</a>）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c"> InterlockedCompareExchange ( __inout LONG volatile *Target,
                                 __in LONG Exchange,
                                 __in LONG Comperand);</pre>
<p><strong>3) C++11中的CAS</strong></p>
<p style="padding-left: 30px;">C++11中的STL中的atomic类的函数可以让你跨平台。（完整的C++11的原子操作可参看 <a href="http://en.cppreference.com/w/cpp/atomic" target="_blank" rel="noopener noreferrer">Atomic Operation Library</a>）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">template&lt; class T &gt;
bool atomic_compare_exchange_weak( std::atomic* obj,
                                   T* expected, T desired );
template&lt; class T &gt;
bool atomic_compare_exchange_weak( volatile std::atomic* obj,
                                   T* expected, T desired );
</pre>
<h4>无锁队列的链表实现</h4>
<p>下面的代码主要参考于两篇论文：</p>
<ul>
<li>John D. Valois 1994年10月在拉斯维加斯的并行和分布系统系统国际大会上的一篇论文——《<a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.53.8674&amp;rep=rep1&amp;type=pdf" target="_blank" rel="noopener noreferrer">Implementing Lock-Free Queues</a>》</li>
<li>美国纽约罗切斯特大学 Maged M. Michael 和 Michael L. Scott 在1996年3月发表的一篇论文 《<a href="https://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf" target="_blank" rel="noopener noreferrer">Simple, Fast, and Practical Non-Blocking and Blocking ConcurrentQueue Algorithms</a>》</li>
</ul>
<p>（注：下面的代码并不完全与这篇论文相同）</p>
<p>初始化一个队列的代码很简，初始化一个dummy结点（注：在链表操作中，使用一个dummy结点，可以少掉很多边界条件的判断），如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">
InitQueue(Q)
{
    node = new node()
    node-&gt;next = NULL;
    Q-&gt;head = Q-&gt;tail = node;
}
</pre>
<p>我们先来看一下进队列用CAS实现的方式，基本上来说就是链表的两步操作：</p>
<ol>
<li>第一步，把tail指针的next指向要加入的结点。 <code>tail-&gt;next = p;</code></li>
<li>第二步，把tail指针移到队尾。 <code>tail = p;</code></li>
</ol>
<pre class="EnlighterJSRAW" data-enlighter-language="c">
EnQueue(Q, data) //进队列
{
    //准备新加入的结点数据
    n = new node();
    n-&gt;value = data;
    n-&gt;next = NULL;

    do {
        p = Q-&gt;tail; //取链表尾指针的快照
    } while( CAS(p-&gt;next, NULL, n) != TRUE); 
    //while条件注释：如果没有把结点链在尾指针上，再试

    CAS(Q-&gt;tail, p, n); //置尾结点 tail = n;
}</pre>
<p>我们可以看到，程序中的那个 do-while 的 Retry-Loop 中的 CAS 操作：如果 <code>p-&gt;next</code> 是 <code>NULL</code>，那么，把新结点 <code>n</code> 加到队尾。如果不成功，则重新再来一次！</p>
<p>就是说，很有可能我在准备在队列尾加入结点时，别的线程已经加成功了，于是tail指针就变了，于是我的CAS返回了false，于是程序再试，直到试成功为止。这个很像我们的抢电话热线的不停重播的情况。</p>
<p>但是你会看到，为什么我们的“置尾结点”的操作（第13行）不判断是否成功，因为：</p>
<ol>
<li>如果有一个线程T1，它的while中的CAS如果成功的话，那么其它所有的 随后线程的CAS都会失败，然后就会再循环，</li>
<li>此时，如果T1 线程还没有更新tail指针，其它的线程继续失败，因为<code>tail-&gt;next</code>不是NULL了。</li>
<li>直到T1线程更新完 <code>tail</code> 指针，于是其它的线程中的某个线程就可以得到新的 <code>tail</code> 指针，继续往下走了。</li>
<li>所以，只要线程能从 while 循环中退出来，意味着，它已经“独占”了，<code>tail</code> 指针必然可以被更新。</li>
</ol>
<p>这里有一个潜在的问题——<strong>如果T1线程在用CAS更新tail指针的之前，线程停掉或是挂掉了，那么其它线程就进入死循环了</strong>。下面是改良版的EnQueue()</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-highlight="10,11">EnQueue(Q, data) //进队列改良版 v1
{
    n = new node();
    n-&gt;value = data;
    n-&gt;next = NULL;

    p = Q-&gt;tail;
    oldp = p
    do {
        while (p-&gt;next != NULL)
            p = p-&gt;next;
    } while( CAS(p.next, NULL, n) != TRUE); //如果没有把结点链在尾上，再试

    CAS(Q-&gt;tail, oldp, n); //置尾结点
}</pre>
<p>我们让每个线程，自己fetch 指针 <code>p</code> 到链表尾。但是这样的fetch会很影响性能。而且，如果一个线程不断的EnQueue，会导致所有的其它线程都去 fetch 他们的 <code>p</code> 指针到队尾，能不能不要所有的线程都干同一个事？这样可以节省整体的时间？</p>
<p>比如：直接 fetch <code>Q-&gt;tail</code> 到队尾？因为，所有的线程都共享着 Q-&gt;tail，所以，一旦有人动了它后，相当于其它的线程也跟着动了，于是，我们的代码可以改进成如下的实现：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">
EnQueue(Q, data) //进队列改良版 v2 
{
    n = new node();
    n-&gt;value = data;
    n-&gt;next = NULL;

    while(TRUE) {
        //先取一下尾指针和尾指针的next
        tail = Q-&gt;tail;
        next = tail-&gt;next;

        //如果尾指针已经被移动了，则重新开始
        if ( tail != Q-&gt;tail ) continue;

        //如果尾指针的 next 不为NULL，则 fetch 全局尾指针到next
        if ( next != NULL ) {
            CAS(Q-&gt;tail, tail, next);
            continue;
        }

        //如果加入结点成功，则退出
        if ( CAS(tail-&gt;next, next, n) == TRUE ) break;
    }
    CAS(Q-&gt;tail, tail, n); //置尾结点
}
</pre>
<p>上述的代码还是很清楚的，相信你一定能看懂，而且，这也是 Java 中的 <code>ConcurrentLinkedQueue</code> 的实现逻辑，当然，我上面的这个版本比 Java 的好一点，因为没有 if 嵌套，嘿嘿。</p>
<p>好了，我们解决了EnQueue，我们再来看看DeQueue的代码：（很简单，我就不解释了）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">
DeQueue(Q) //出队列
{
    do{
        p = Q-&gt;head;
        if (p-&gt;next == NULL){
            return ERR_EMPTY_QUEUE;
        }
    while( CAS(Q-&gt;head, p, p-&gt;next) != TRUE );
    return p-&gt;next-&gt;value;
}</pre>
<p><strong>我们可以看到，DeQueue的代码操作的是 <code>head-&gt;next</code>，而不是 <code>head</code> 本身。这样考虑是因为一个边界条件，我们需要一个dummy的头指针来解决链表中如果只有一个元素，<code>head</code> 和 <code>tail</code> 都指向同一个结点的问题，这样 <code>EnQueue</code> 和 <code>DeQueue</code> 要互相排斥了</strong>。</p>
<p>但是，如果 <code>head</code> 和 <code>tail</code> 都指向同一个结点，这意味着队列为空，应该返回 <code>ERR_EMPTY_QUEUE</code>，但是，在判断 <code>p-&gt;next == NULL</code> 时，另外一个EnQueue操作做了一半，此时的 p-&gt;next 不为 NULL了，但是 tail 指针还差最后一步，没有更新到新加的结点，这个时候就会出现，在 EnQueue 并没有完成的时候， DeQueue 已经把新增加的结点给取走了，此时，队列为空，但是，head 与 tail 并没有指向同一个结点。如下所示：</p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-20047" src="https://coolshell.cn/wp-content/uploads/2012/09/lock.free_.queue_-224x300.png" alt="" width="400" height="537" srcset="https://coolshell.cn/wp-content/uploads/2012/09/lock.free_.queue_-224x300.png 224w, https://coolshell.cn/wp-content/uploads/2012/09/lock.free_.queue_-201x270.png 201w, https://coolshell.cn/wp-content/uploads/2012/09/lock.free_.queue_.png 706w" sizes="(max-width: 400px) 100vw, 400px" /></p>
<p>虽然，EnQueue的函数会把 tail 指针置对，但是，这种情况可能还是会导致一些并发问题，所以，严谨来说，我们需要避免这种情况。于是，我们需要加入更多的判断条件，还确保这个问题。下面是相关的改进代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">
DeQueue(Q) //出队列，改进版
{
    while(TRUE) {
        //取出头指针，尾指针，和第一个元素的指针
        head = Q-&gt;head;
        tail = Q-&gt;tail;
        next = head-&gt;next;

        // Q-&gt;head 指针已移动，重新取 head指针
        if ( head != Q-&gt;head ) continue;
        
        // 如果是空队列
        if ( head == tail &amp;&amp; next == NULL ) {
            return ERR_EMPTY_QUEUE;
        }
        
        //如果 tail 指针落后了
        if ( head == tail &amp;&amp; next == NULL ) {
            CAS(Q-&gt;tail, tail, next);
            continue;
        }

        //移动 head 指针成功后，取出数据
        if ( CAS( Q-&gt;head, head, next) == TRUE){
            value = next-&gt;value;
            break;
        }
    }
    free(head); //释放老的dummy结点
    return value;
}</pre>
<p>上面这段代码的逻辑和 Java 的 <code>ConcurrentLinkedQueue</code> 的 <code>poll</code> 方法很一致了。也是《<a href="https://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf" target="_blank" rel="noopener noreferrer">Simple, Fast, and Practical Non-Blocking and Blocking ConcurrentQueue Algorithms</a>》这篇论文中的实现。</p>
<h4>CAS的ABA问题</h4>
<p>所谓ABA（<a href="http://en.wikipedia.org/wiki/ABA_problem" target="_blank" rel="noopener noreferrer">见维基百科的ABA词条</a>），问题基本是这个样子：</p>
<ol>
<li>进程P1在共享变量中读到值为A</li>
<li>P1被抢占了，进程P2执行</li>
<li>P2把共享变量里的值从A改成了B，再改回到A，此时被P1抢占。</li>
<li>P1回来看到共享变量里的值没有被改变，于是继续执行。</li>
</ol>
<p>虽然P1以为变量值没有改变，继续执行了，但是这个会引发一些潜在的问题。<strong>ABA问题最容易发生在lock free 的算法中的，CAS首当其冲，因为CAS判断的是指针的值。很明显，值是很容易又变成原样的。</strong></p>
<p>比如上述的DeQueue()函数，因为我们要让head和tail分开，所以我们引入了一个dummy指针给head，当我们做CAS的之前，如果head的那块内存被回收并被重用了，而重用的内存又被EnQueue()进来了，这会有很大的问题。（<strong>内存管理中重用内存基本上是一种很常见的行为</strong>）</p>
<p>这个例子你可能没有看懂，维基百科上给了一个活生生的例子——</p>
<blockquote><p>你拿着一个装满钱的手提箱在飞机场，此时过来了一个火辣性感的美女，然后她很暖昧地挑逗着你，并趁你不注意的时候，把用一个一模一样的手提箱和你那装满钱的箱子调了个包，然后就离开了，你看到你的手提箱还在那，于是就提着手提箱去赶飞机去了。</p></blockquote>
<p>这就是ABA的问题。</p>
<h4>解决ABA的问题</h4>
<p>维基百科上给了一个解——使用double-CAS（双保险的CAS），例如，在32位系统上，我们要检查64位的内容</p>
<p style="padding-left: 30px;">1）一次用CAS检查双倍长度的值，前半部是值，后半部分是一个计数器。</p>
<p style="padding-left: 30px;">2）只有这两个都一样，才算通过检查，要吧赋新的值。并把计数器累加1。</p>
<p>这样一来，ABA发生时，虽然值一样，但是计数器就不一样（但是在32位的系统上，这个计数器会溢出回来又从1开始的，这还是会有ABA的问题）</p>
<p>当然，我们这个队列的问题就是不想让那个内存重用，这样明确的业务问题比较好解决，论文《<a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.53.8674&amp;rep=rep1&amp;type=pdf" target="_blank" rel="noopener noreferrer">Implementing Lock-Free Queues</a>》给出一这么一个方法——<strong>使用结点内存引用计数refcnt</strong>！（论文《<a href="https://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf" target="_blank" rel="noopener noreferrer">Simple, Fast, and Practical Non-Blocking and Blocking ConcurrentQueue Algorithms</a>》中的实现方法也基本上是一样的，用到的是增加一个计数，可以理解为版本号）</p>
<p>）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-highlight="9,14">SafeRead(q)
{
    loop:
        p = q-&gt;next;
        if (p == NULL){
            return p;
        }

        Fetch&amp;Add(p-&gt;refcnt, 1);

        if (p == q-&gt;next){
            return p;
        }else{
            Release(p);
        }
    goto loop;
}</pre>
<p>其中的 Fetch&amp;Add和Release分是是加引用计数和减引用计数，都是原子操作，这样就可以阻止内存被回收了。</p>
<h4>用数组实现无锁队列</h4>
<p>本实现来自论文《<a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.53.8674&amp;rep=rep1&amp;type=pdf" target="_blank" rel="noopener noreferrer">Implementing Lock-Free Queues</a>》</p>
<p>使用数组来实现队列是很常见的方法，因为没有内存的分部和释放，一切都会变得简单，实现的思路如下：</p>
<p style="padding-left: 30px;">1）数组队列应该是一个ring buffer形式的数组（环形数组）</p>
<p style="padding-left: 30px;">2）数组的元素应该有三个可能的值：HEAD，TAIL，EMPTY（当然，还有实际的数据）</p>
<p style="padding-left: 30px;">3）数组一开始全部初始化成EMPTY，有两个相邻的元素要初始化成HEAD和TAIL，这代表空队列。</p>
<p style="padding-left: 30px;">4）EnQueue操作。假设数据x要入队列，定位TAIL的位置，使用double-CAS方法把(TAIL, EMPTY) 更新成 (x, TAIL)。需要注意，如果找不到(TAIL, EMPTY)，则说明队列满了。</p>
<p style="padding-left: 30px;">5）DeQueue操作。定位HEAD的位置，把(HEAD, x)更新成(EMPTY, HEAD)，并把x返回。同样需要注意，如果x是TAIL，则说明队列为空。</p>
<p>算法的一个关键是——如何定位HEAD或TAIL？</p>
<p style="padding-left: 30px;">1）我们可以声明两个计数器，一个用来计数EnQueue的次数，一个用来计数DeQueue的次数。</p>
<p style="padding-left: 30px;">2）这两个计算器使用使用Fetch&amp;ADD来进行原子累加，在EnQueue或DeQueue完成的时候累加就好了。</p>
<p style="padding-left: 30px;">3）累加后求个模什么的就可以知道TAIL和HEAD的位置了。</p>
<p>如下图所示：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-8240" title="Lock-Free Queue(Array)" src="https://coolshell.cn/wp-content/uploads/2012/09/lock-free-array.jpg" alt="" width="477" height="215" srcset="https://coolshell.cn/wp-content/uploads/2012/09/lock-free-array.jpg 477w, https://coolshell.cn/wp-content/uploads/2012/09/lock-free-array-300x135.jpg 300w" sizes="(max-width: 477px) 100vw, 477px" /></p>
<h4 style="text-align: left;"> 小结</h4>
<p style="text-align: left;">以上基本上就是所有的无锁队列的技术细节，这些技术都可以用在其它的无锁数据结构上。</p>
<p style="text-align: left; padding-left: 30px;">1）无锁队列主要是通过CAS、FAA这些原子操作，和Retry-Loop实现。</p>
<p style="text-align: left; padding-left: 30px;">2）对于Retry-Loop，我个人感觉其实和锁什么什么两样。只是这种“锁”的粒度变小了，主要是“锁”HEAD和TAIL这两个关键资源。而不是整个数据结构。</p>
<p style="text-align: left;">还有一些和Lock Free的文章你可以去看看：</p>
<ul>
<li>Code Project 上的雄文 《<a href="http://www.codeproject.com/Articles/153898/Yet-another-implementation-of-a-lock-free-circular" target="_blank" rel="noopener noreferrer">Yet another implementation of a lock-free circular array queue</a>》</li>
<li>Herb Sutter的《<a href="http://www.drdobbs.com/parallel/writing-lock-free-code-a-corrected-queue/210604448?pgno=1" target="_blank" rel="noopener noreferrer">Writing Lock-Free Code: A Corrected Queue</a>》&#8211; 用C++11的std::atomic模板。</li>
<li>IBM developerWorks的《<a href="http://www.ibm.com/developerworks/cn/aix/library/au-multithreaded_structures2/index.html" target="_blank" rel="noopener noreferrer">设计不使用互斥锁的并发数据结构</a>》</li>
</ul>
<div>【<strong>注：我配了一张look-free的自行车，寓意为——如果不用专门的车锁，那么自行得自己锁自己！</strong>】</div>
<p style="text-align: left;"> （全文完）</p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/10975.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/0.jpg" alt="一个“蝇量级” C 语言协程库" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10975.html" class="wp_rp_title">一个“蝇量级” C 语言协程库</a></li><li ><a href="https://coolshell.cn/articles/9886.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/8.jpg" alt="二叉树迭代器算法" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9886.html" class="wp_rp_title">二叉树迭代器算法</a></li><li ><a href="https://coolshell.cn/articles/7886.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/07/muxnt-150x150.jpg" alt="代码执行的效率" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7886.html" class="wp_rp_title">代码执行的效率</a></li><li ><a href="https://coolshell.cn/articles/6548.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/02/WhyCPP.01-150x150.jpg" alt="Why C++ ? 王者归来" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6548.html" class="wp_rp_title">Why C++ ? 王者归来</a></li><li ><a href="https://coolshell.cn/articles/6010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/23.jpg" alt="一些有意思的算法代码" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6010.html" class="wp_rp_title">一些有意思的算法代码</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/8239.html">无锁队列的实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/8239.html/feed</wfw:commentRss>
			<slash:comments>241</slash:comments>
		
		
			</item>
	</channel>
</rss>
