并发编程(三): 使用C++11实现无锁stack(lock-free stack)
前几篇文章,我们讨论了如何使用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)的更多相关文章
- 【Java并发编程实战】-----“J.U.C”:锁,lock
在java中有两种方法实现锁机制,一种是在前一篇博客中([java7并发编程实战]-----线程同步机制:synchronized)介绍的synchronized,而另一种是比synchronized ...
- java并发编程实战《二十一》无锁工具类
不安全的累加代码,如下 1 public class Test { 2 long count = 0; 3 void add10K() { 4 int idx = 0; 5 while(idx++ & ...
- 【java并发编程】十三章:显式锁:LOCK
java5以后,新增了显式锁,用于当内置锁不能满足需求后可选择的一种高级方案. lock接口的特点 与内置锁一样,他能提供互斥性,内存可见性,可重入等特征,与内置锁不同的是,Lock提供了一种无条件, ...
- Java并发编程三个性质:原子性、可见性、有序性
并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确 线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的 ...
- Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- 并发编程入门(三): 使用C++11实现无锁stack(lock-free stack)
前几篇文章,我们讨论了如何使用mutex保护数据及使用使用condition variable在多线程中进行同步.然而,使用mutex将会导致一下问题: 等待互斥锁会消耗宝贵的时间 - 有时候是很多时 ...
- 从缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析案例
引入高速缓存概念 在计算机在执行程序时,以指令为单位来执行,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入. 由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这 ...
- 【Java并发编程三】闭锁
1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...
- Java并发编程(十)-- Java中的锁
在学习或者使用Java的过程中进程会遇到各种各样的锁的概念:公平锁.非公平锁.自旋锁.可重入锁.偏向锁.轻量级锁.重量级锁.读写锁.互斥锁.死锁.活锁等,本文将简概的介绍一下各种锁. 公平锁和非公平锁 ...
随机推荐
- Java中的String,StringBuilder,StringBuffer三者的区别
最近在学习Java的时候,遇到了这样一个问题,就是String,StringBuilder以及StringBuffer这三个类之间有什么区别呢,自己从网上搜索了一些资料,有所了解了之后在这里整理一下, ...
- 微信小程序开发 导入文件说没找到.json的问题
这个真的坑爹 网上的答案也没几个靠谱 说白了就是找个空文件直接创建 然后复制粘贴 反应了这玩意ide不成熟 进去之后直接创了个quick start的源码 也就是我们看的这个网页建议我们下载的源码 老 ...
- Redis持久化的两种方式(RDB和AOF)
redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File). RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储 ...
- Python小代码_2_格式化输出
Python小代码_2_格式化输出 name = input("name:") age = input("age:") job = input("jo ...
- 15. 3Sum(中等)
Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all un ...
- sublime下配置C/C++运行环境
最近在学习<WEB前端课程>老师教我们使用DW,但是不太喜欢,就选择了sublime,写前端代码还是很方便. 平时都是写C++,C比较多,借鉴了别人的配置步骤,将sublime打造成IDE ...
- Java中的网络支持InetAddress&URL
针对网络通信的不同层次,Java提供的网络功能有四大类 InetAddress:用于标识网络上的硬件资源.(说白了就是IP地址的相关信息) URL:统一资源定位符,通过URL可以直接读取或写入网络上的 ...
- zookeeper基本原理及适用场景 转:http://blog.chinaunix.net/uid-26748613-id-4536290.html
1.1 zookeeper简介 Zookeeper 是 Hadoop 生态系统中的协同实现,是Hadoop集群管理的一个必不可少的模块,它主要来控制集群中的数据,如它管理Hadoop集群中的NameN ...
- Redis之(五)持久化
Redis提供了两种持久化的方式: (1)RDB(Redis DataBase)模式,就是在不同的时间点,将Redis存储的数据生成快照并存储到磁盘等介质上: (2)AOF(Append Only F ...
- android M Launcher之LauncherModel (二)
上一篇我们通过LauncherModel的创建 ,实例化,以及与LauncherModel之间的沟通方式.初步了解了LauncherModel一些功能及用法,如果对LauncherModel一系列初始 ...