前几篇文章,我们讨论了如何使用mutex保护数据及使用使用condition variable在多线程中进行同步。然而,使用mutex将会导致一下问题:

  • 等待互斥锁会消耗宝贵的时间 — 有时候是很多时间。这种延迟会损害系统的scalability。尤其是在现在可用的core越多越多的情况下。
  • 低优先级的线程可以获得互斥锁,因此阻碍需要同一互斥锁的高优先级线程。这个问题称为优先级倒置(priority inversion )
  • 可能因为分配的时间片结束,持有互斥锁的线程被取消调度。这对于等待同一互斥锁的其他线程有不利影响,因为等待时间现在会更长。这个问题称为锁护送(lock convoying)

互斥锁的问题还不只这些。早在1994年10月,John D. Valois 在拉斯维加斯的并行和分布系统系统国际大会上的一篇论文—《Implementing Lock-Free Queues》已经研究了无锁队列的实现,有兴趣的可以拜读一下。

实现无锁数据结构的基础是CAS:Compare & Set,或是 Compare & Swap。CAS用C语言描述的代码(来自Wikipedia Compare And Swap)

int compare_and_swap (int* reg, int oldval, int newval)
{
ATOMIC();
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
END_ATOMIC();
return old_reg_val;
}

CAS是个原子操作,保证了如果需要更新的地址没有被他人改动多,那么它可以安全的写入。而这也是我们对于某个数据或者数据结构加锁要保护的内容,保证读写的一致性,不出现dirty data。现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是 CMPXCHG 汇编指令现在,我们将使用CAS来实现无锁的stack,然后你就能够理解CAS的用法了。

C++11中CAS实现:

template< class T>
struct atomic
{
public:
bool compare_exchange_weak( T& expected, T desired,
std::memory_order success,
std::memory_order failure );
bool compare_exchange_weak( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) volatile;
bool compare_exchange_weak( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst );
bool compare_exchange_weak( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) volatile;
bool compare_exchange_strong( T& expected, T desired,
std::memory_order success,
std::memory_order failure );
bool compare_exchange_strong( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) volatile;
bool compare_exchange_strong( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst );
bool compare_exchange_strong( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) volatile;
...
};

Please refer to http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange to more information.

对上面的版本进行一下说明。翻译自上述url:

Atomically compares the value stored in *this with the value of
expected
, and if those are equal, replaces the former with desired (performs read-modify-write operation). Otherwise, loads the actual value stored in*this intoexpected (performs load operation).

自动的比较*this的值和expect的值,如果相等,那么将*this的值替换为desired的值(进行读-修改-写操作)。否则如果不相等,那么将*this的值存到expected处。

伪码就是:

if *this == expected:

    *this = desired;

else:

    expected = *this;

The memory models for the read-modify-write and load operations aresuccess andfailure respectively. In the (2) and (4) versionsorder is used for both read-modify-write and load operations, except thatstd::memory_order_release
andstd::memory_order_relaxed are used for the load operation iforder==std::memory_order_acq_rel,
ororder==std::memory_order_release
respectively.

success对应于read-modify-write的内存模型;failure则对应于失败时的load。对于order
= std::memory_order_seq_cst的函数,那么该memory order适用于read-modify-write and load,除非是如果order==std::memory_order_acq_rel,那么load将使用std::memory_order_release;如果order==std::memory_order_release,那么load将使用std::memory_order_relaxed

更多信息memory order请阅读:http://en.cppreference.com/w/cpp/atomic/memory_order

The weak forms (1-2) of the functions are allowed to fail spuriously, that is, act as if*this!= expected even
if they are equal. When a compare-and-exchange is in a loop, the weak version will yield better performance on some platforms. When a weak compare-and-exchange would require a loop and a strong one would not, the strong one is preferable.

weak形式允许假失败,该函数直接比较原子对象所封装的值与参数 expected 的物理内容,所以某些情况下,对象的比较操作在使用 operator==() 判断时相等,但 compare_exchange_weak 判断时却可能失败,因为对象底层的物理内容中可能存在位对齐或其他逻辑表示相同但是物理表示不同的值(比如 true 和 2 或 3,它们在逻辑上都表示"真",但在物理上两者的表示并不相同)。可以虚假的返回false(和expected相同)。若本atomic的T值和expected相同则用val值替换本atomic的T值,返回true;若不同则用本atomic的T值替换expected,返回false。
 

与compare_exchange_weak 不同, strong版本的 compare-and-exchange 操作不允许(spuriously 地)返回 false,即原子对象所封装的值与参数 expected 的物理内容相同,比较操作一定会为 true。不过在某些平台下,如果算法本身需要循环操作来做检查, compare_exchange_weak 的性能会更好。因此对于某些不需要采用循环操作的算法而言, 通常采用compare_exchange_strong 更好

下面代码部分来自http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange。

#include <atomic>
#include <string>
#include <iostream>
using namespace std;
template<typename T>
struct node
{
T data;
node* next;
node(const T& data) : data(data), next(nullptr) {}
}; template<typename T>
class stack
{
std::atomic<node<T>*> head; public:
stack():head(nullptr){}
void push(const T& data);
T pop();
};

注意在这里添加了stack的构造函数,把head初始化为nullptr。如果不初始化它为nullptr,那么使用链表存储的stack将无法确定终点在哪儿。。。

首先看一下push的实现:

    void push(const T& data)
{
node<T>* new_node = new node<T>(data); // put the current value of head into new_node->next
new_node->next = head.load(std::memory_order_relaxed); // now make new_node the new head, but if the head
// is no longer what's stored in new_node->next
// (some other thread must have inserted a node just now)
// then put that new head into new_node->next and try again
while(!head.compare_exchange_weak(new_node->next,
new_node,
std::memory_order_release,
std::memory_order_relaxed))
; // the body of the loop is empty
}

主要是理解这两句:

head.compare_exchange_weak(new_node->next,
new_node,

可以简单用一下代码来概括该调用的效果:

if ( head == new_node->new){
head = new_node;
return true;
}
else{
new_node->next = head;
return false;
}

因此,如果没有其他的线程push,那么head将指向当前的new_node,push完成。否则,说明其他线程push过新数据,那么将当前push的新节点重新放到顶端,此时的head是最新的head。这样,通过CAS,我们可以实现了thread-safe stack。

接下来看一下pop:

    T pop()
{
while(1){
auto result = head.load(std::memory_order_relaxed);
if (result == nullptr)
throw std::string("Cannot pop from empty stack");
if(head.compare_exchange_weak(result,result->next,
std::memory_order_release,
std::memory_order_relaxed))
return result->data;
}
}

我们为什么要限制result != nullptr?因为有可能当前stack仅有一个元素,线程B在pop时被调度,线程A pop成功,那么线程B再pop就会出问题。

其实,上述的pop可以简化,因为result其实在failed时候已经更新为head了。因此简化代码可以是:

  T pop()
{
auto result = head.load(std::memory_order_relaxed);
while( result != nullptr && !head.compare_exchange_weak(result,result->next,
std::memory_order_release,
std::memory_order_relaxed));
if( result != nullptr)
return result->data;
else
throw std::string("Cannot pop from empty stack");
}

尊重原创,转载请注明出处: anzhsoft http://blog.csdn.net/anzhsoft/article/details/19125619

参考资料:

1. http://en.wikipedia.org/wiki/Compare-and-swap

2. http://en.wikipedia.org/wiki/Fetch-and-add

3. http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange

4. http://technet.microsoft.com/zh-cn/hh874698

更多学习:

1. GCC实现 http://www.oschina.net/translate/a-fast-lock-free-queue-for-cpp?cmp

2. GCC实现 http://www.ibm.com/developerworks/cn/aix/library/au-multithreaded_structures2/index.html

陈皓同学的精彩博文: http://coolshell.cn/articles/8239.html

并发编程入门(三): 使用C++11实现无锁stack(lock-free stack)的更多相关文章

  1. Java并发编程入门与高并发面试(三):线程安全性-原子性-CAS(CAS的ABA问题)

    摘要:本文介绍线程的安全性,原子性,java.lang.Number包下的类与CAS操作,synchronized锁,和原子性操作各方法间的对比. 线程安全性 线程安全? 线程安全性? 原子性 Ato ...

  2. 脑残式网络编程入门(三):HTTP协议必知必会的一些知识

    本文原作者:“竹千代”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.前言 无论是即时通讯应用还是传统的信息系统,Http协议都是我们最常打交 ...

  3. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

  4. 并发编程(三)Promise, Future 和 Callback

    并发编程(三)Promise, Future 和 Callback 异步操作的有两个经典接口:Future 和 Promise,其中的 Future 表示一个可能还没有实际完成的异步任务的结果,针对这 ...

  5. Java并发编程原理与实战四十二:锁与volatile的内存语义

    锁与volatile的内存语义 1.锁的内存语义 2.volatile内存语义 3.synchronized内存语义 4.Lock与synchronized的区别 5.ReentrantLock源码实 ...

  6. [并发编程 - 多线程:信号量、死锁与递归锁、时间Event、定时器Timer、线程队列、GIL锁]

    [并发编程 - 多线程:信号量.死锁与递归锁.时间Event.定时器Timer.线程队列.GIL锁] 信号量 信号量Semaphore:管理一个内置的计数器 每当调用acquire()时内置计数器-1 ...

  7. 并发编程(三): 使用C++11实现无锁stack(lock-free stack)

    前几篇文章,我们讨论了如何使用mutex保护数据及使用使用condition variable在多线程中进行同步.然而,使用mutex将会导致一下问题: 等待互斥锁会消耗宝贵的时间 - 有时候是很多时 ...

  8. 【Java并发编程】6、volatile关键字解析&内存模型&并发编程中三概念

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...

  9. Java并发编程(三)volatile域

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Android多线程(一)线程池 Android多线程(二)AsyncTask源代码分析 前言 有时仅仅为了读写一个或 ...

随机推荐

  1. hdu 1029 求出现次数过半的数

    题目传送门//res tp hdu 已知必定存在一个元素出现次数超过一半,考虑用栈 若当前元素等于栈顶元素,入栈,反之出栈,并将当前元素入栈 最终的栈顶元素即是所求 #include<iostr ...

  2. Centos7下,宿主机nginx配合docker环境的php-fpm

    一.安装docker并启动 yum install docker systemctl start docker 二.安装nginxCentOS 7默认不能从yum中安装nginx,原因可以自己搜索一下 ...

  3. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by de

    MySQL在高版本需要指明是否进行SSL连接 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/framework?characterEncoding ...

  4. 01 Java 内存分配全面浅析

    http://blog.csdn.net/shimiso/article/details/8595564 Java 内存分配全面浅析  本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的 ...

  5. git bash配置SSH远程连接阿里云ECS

    1.连接配置 1-1.添加安全组规则 1-2.使用GitHub的话本地都会有id_rsa.pub(公钥),id_rsa(私钥),一般保存在C盘用户目录下.ssh文件夹. 把公钥内容复制下来(ssh-r ...

  6. lamp :在Linux 下搭建apache、Mysql、php

    CentOS下搭建LAMP环境 LAMP: Linux + Apache + PHP + Mysql. 系统: CentOS 7,64位. CentOS安装 我选取了64位的CentOS 7这个Lin ...

  7. 关于SpringMVC的几件小事

    一.SpringMVC表单标签He处理静态资源 1.Spring的表单标签 通过SpringMVC的表单标签可以实现将模型数据中的属性和HTML表单元素相绑定,以实现表单数据更便捷编辑和表单值的回显. ...

  8. ES6入门九:Symbol元编程

    JS第七种数据类型:Symbol Symbol的应用场景 11个Symbol静态属性 Symbol元编程 一.JS第七种数据类型:Symbol 在ES6之前的JavaScript的基本数据类型有und ...

  9. 【小知识点】去除inline-block元素间间距的办法

    之前一直用float浮动方法布局,因为display:inline-block有间隙,现在找到办法了.在父元素上面加font-sise:0,就可以了. 效果如图: 代码如下: <!DOCTYPE ...

  10. footer始终在页面最底部的方法(问题待检验)

    一.css方法 <style type="text/css"> html,body{ height: 100%; } body{ display: flex; flex ...