1、概要

本文是无锁同步系列文章的第一篇,主要探讨C++11中的Atomic。

我们知道在C++11中引入了mutex和方便优雅的lock_guard。但是有时候我们想要的是性能更高的无锁实现,下面我们来讨论C++11中新增的原子操作类Atomic,我们可以利用它巧妙地实现无锁同步。

2、传统的线程同步

 #include <thread>
#include <mutex> #include <iostream> using namespace std; mutex g_mutex;
int g_count = ; int main()
{
thread thr1([]() {
for (int i = ;i < ;i++) {
lock_guard<mutex> lock(g_mutex); //①
g_count += ;
}
}); thread thr2([]() {
for (int i = ;i < ;i++) {
lock_guard<mutex> lock(g_mutex); //②
g_count += ;
}
}); thr1.join();
thr2.join(); cout << g_count << endl; }

在上述例子中,如果把①②的锁注释后,我们可能无法得到正确的结果。原因是C++并没有给我们保证+=操作具有原子性(其本质应该是读-加-写3个操作)。

3、Atomic

C++11给我们带来的Atomic一系列原子操作类,它们提供的方法能保证具有原子性。这些方法是不可再分的,获取这些变量的值时,永远获得修改前的值或修改后的值,不会获得修改过程中的中间数值。

这些类都禁用了拷贝构造函数,原因是原子读和原子写是2个独立原子操作,无法保证2个独立的操作加在一起仍然保证原子性。

这些类中,最简单的是atomic_flag(其实和atomic<bool>相似),它只有test_and_set()和clear()方法。其中,test_and_set会检查变量的值是否为false,如果为false则把值改为true。

除了atomic_flag外,其他类型可以通过atomic<T>获得。atomic<T>提供了常见且容易理解的方法:

  1. store
  2. load
  3. exchange
  4. compare_exchange_weak
  5. compare_exchange_strong

store是原子写操作,而load则是对应的原子读操作。

exchange允许2个数值进行交换,并保证整个过程是原子的。

而compare_exchange_weak和compare_exchange_strong则是著名的CAS(compare and set)。参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。

weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。

3、例子

下面举个简单的例子,使用CAS操作实现一个不带锁的并发栈。这个例子从《C++并发编程》摘抄而来。

Push

在非并发条件下,要实现一个栈的Push操作,我们可能有如下操作:

    1. 新建一个节点
    2. 将该节点的next指针指向现有栈顶
    3. 更新栈顶

但是在并发条件下,上述无保护的操作明显可能出现问题。下面举一个例子:

  1. 原栈顶为A。(此时栈状态: A->P->Q->...,我们约定从左到右第一个值为栈顶,P->Q代表p.next = Q)
  2. 线程1准备将B压栈。线程1执行完步骤2后被强占。(新建节点B,并使 B.next = A,即B->A)
  3. 线程2得到cpu时间片并完成将C压栈的操作,即完成步骤1、2、3。此时栈状态(此时栈状态: C->A->...)
  4. 这时线程1重新获得cpu时间片,执行步骤3。导致栈状态变为(此时栈状态: B->A->...)

结果线程2的操作丢失,这显然不是我们想要的结果。

那么我们如何解决这个问题呢?只要保证步骤3更新栈顶时候,栈顶是我们在步骤2中获得顶栈顶即可。因为如果有其它线程进行操作,栈顶必然改变。

我们可以利用CAS轻松解决这个问题:如果栈顶是我们步骤2中获取顶栈顶,则执行步骤3。否则,自旋(即重新执行步骤2)。

因此,不带锁的压栈Push操作比较简单。

 template<typename T>
class lock_free_stack
{
private:
struct node
{
T data;
node* next; node(T const& data_):
data(data_)
{}
}; std::atomic<node*> head;
public:
void push(T const& data)
{
node* const new_node=new node(data);
new_node->next=head.load();
while(!head.compare_exchange_weak(new_node->next,new_node));
}
};

我们可以注意到一个非常巧妙的设计。在push方法里,atomic_compare_exchange_weak如果失败,证明有其他线程更新了栈顶,而这个时候被其他线程更新的新栈顶值会被更新到new_node->next中,因此循环可以直接再次尝试压栈而无需由程序员更新new_node->next。

Pop

2017.3.14:

发现原文Pop部分有错误,所以暂时删除

无锁同步-C++11之Atomic和CAS的更多相关文章

  1. 无锁同步-JAVA之Volatile、Atomic和CAS

    1.概要 本文是无锁同步系列文章的第二篇,主要探讨JAVA中的原子操作,以及如何进行无锁同步. 关于JAVA中的原子操作,我们很容易想到的是Volatile变量.java.util.concurren ...

  2. java 多线程12 : 无锁 实现CAS原子性操作----原子类

    由于java 多线程11:volatile关键字该文讲道可以使用不带锁的情况也就是无锁使变量变成可见,这里就理解下如何在无锁的情况对线程变量进行CAS原子性及可见性操作 我们知道,在并发的环境下,要实 ...

  3. 二、多线程基础-乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁

    1.10乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将 比较-设置 ...

  4. CAS无锁机制原理

    原子类 java.util.concurrent.atomic包:原子类的小工具包,支持在单个变量上解除锁的线程安全编程 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读 ...

  5. CAS原子操作实现无锁及性能分析

    CAS原子操作实现无锁及性能分析 Author:Echo Chen(陈斌) Email:chenb19870707@gmail.com Blog:Blog.csdn.net/chen19870707 ...

  6. 无锁编程以及CAS

    无锁编程 / lock-free / 非阻塞同步 无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Sy ...

  7. 4.锁--无锁编程以及CAS

    无锁编程以及CAS 无锁编程 / lock-free / 非堵塞同步 无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被堵塞的情况下实现变量的同步,所以也叫非堵塞同步(Non-b ...

  8. 【多线程】无锁编程以及CAS

    无锁编程 / lock-free / 非阻塞同步 无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Sy ...

  9. Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

    精彩理解:  https://www.jianshu.com/p/21be831e851e ;  https://blog.csdn.net/heyutao007/article/details/19 ...

随机推荐

  1. 判断iPhone/android手机

    JS判断请求来自Android手机还是iPhone手机,根据不同的手机跳转到不同的链接. var browser = {versions: function () {var u = navigator ...

  2. JavaBean--实例:注册验证

    通过JSP+JavaBean完成一个注册用户的验证功能: index.jsp: 注册信息填写页,同时对错误数据进行错误提示 check.jsp:将输入表单数据自动赋值给JavaBean,同时验证,失败 ...

  3. C#入门经典(1)

  4. iOS的基本图形绘制

    绘图的步骤: 1.获取上下文 2.创建路径(描述路径) 3.把路径添加到上下文 4.渲染上下文 通常在- (void)drawRect:(CGRect)rect这个方法里面绘制图形 为什么要再draw ...

  5. ormlite 批处理操作

    提高ormlite的批处理速度 http://stackoverflow.com/quegoogstions/11761472/ormlites-createorupdate-seems-slow-w ...

  6. java中单例设计模式

    在java中创建单例的方式主要有三种:饿汉式.懒汉式.登记式.以下内容均是摘抄自 http://blog.csdn.net/jason0539/article/details/23297037/ 一. ...

  7. Andriod学习笔记 - 参考

    Andriod学习笔记 - 参考 自定义实现圆形播放进度条(android,飞一般的感觉) 盘点Android开发者必备的十大开发工具

  8. ural1494 Monobilliards

    Monobilliards Time limit: 1.0 secondMemory limit: 64 MB A monobilliards table set up in a gaming hou ...

  9. 如何用70行Java代码实现深度神经网络算法

    http://www.tuicool.com/articles/MfYjQfV 如何用70行Java代码实现深度神经网络算法 时间 2016-02-18 10:46:17  ITeye 原文  htt ...

  10. struts2-----新建项目

    1. 建立界面原型 2. 建立Struts.xml 确定namespace, package, action, 空方法, result, 界面原型修改, 匹配现有设置, 测试, 做好规划 3. 建立数 ...