用原子操作实现无锁编程[通俗易懂]

用原子操作实现无锁编程[通俗易懂]假设我们要维护一个全局的线程安全的int类型变量count,下面这两行代码都是很危险的:count++;count+=n;我们知道,高级语言中的一条语句,并不是一个原子操作.比如一个最简单的自增操作就分为三步: 1.从缓存取到寄存器2.在寄存器加13.存入缓存。多个线程访问同一块内存时,需要加锁来保证访问操作是互斥的. 所以,我

大家好,又见面了,我是你们的朋友全栈君。

假设我们要维护一个全局的线程安全的 int 类型变量 count, 下面这两行代码都是很危险的:

count ++;

count += n;

我们知道, 高级语言中的一条语句, 并不是一个原子操作. 比如一个最简单的自增操作就分为三步: 

1. 从缓存取到寄存器
2. 在寄存器加1
3. 存入缓存。


多个线程访问同一块内存时, 需要加锁来保证访问操作是互斥的. 

所以, 我们可以在操作 count 的时候加一个互斥锁. 如下面的代码:

pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&count_lock);
count++;
pthread_mutex_unlock(&count_lock);


另一个办法就是, 让 count++ 和 count+=n 这样的语句变成原子操作. 一个原子操作必然是线程安全的. 有两种使用原子操作的方式:

1. 使用 gcc 的原子操作

2. 使用 c++11中STL中的 stomic 类的函数

在这里我只介绍 gcc 里的原子操作, 这些函数分成以下几组:

type __sync_fetch_and_add (type *ptr, type value, …)
type __sync_fetch_and_sub (type *ptr, type value, …)
type __sync_fetch_and_or (type *ptr, type value, …)
type __sync_fetch_and_and (type *ptr, type value, …)
type __sync_fetch_and_xor (type *ptr, type value, …)
type __sync_fetch_and_nand (type *ptr, type value, …)
返回更新前的值

type __sync_add_and_fetch (type *ptr, type value, …)
type __sync_sub_and_fetch (type *ptr, type value, …)
type __sync_or_and_fetch (type *ptr, type value, …)
type __sync_and_and_fetch (type *ptr, type value, …)
type __sync_xor_and_fetch (type *ptr, type value, …)
type __sync_nand_and_fetch (type *ptr, type value, …)
返回更新后的值

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, …)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, …)
这两个函数提供原子的比较和交换,如果*ptr == oldval,就将newval写入*ptr,
第一个函数在相等并写入的情况下返回true.
第二个函数在返回操作之前的值。 

type __sync_lock_test_and_set (type *ptr, type value, …)
将*ptr设为value并返回*ptr操作之前的值。

void __sync_lock_release (type *ptr, …)
将*ptr置0 

因为 gcc 具体实现的问题, 后面的可扩展参数 (…) 没有什么用, 可以省略掉.
gcc 保证了这些接口都是原子的. 调用这些接口时, 前端串行总线会被锁住, 锁住了它, 其它 cpu 就不能从存储器获取数据. 从而保证对内存操作的互斥. 当然, 这种操作是有不小代价, 所以只能在操作小的内存才可以这么做. 上面的接口使用的 type 只能是 1, 2, 4 或 8 字节的整形, 
即:
int8_t / uint8_t
int16_t / uint16_t
int32_t / uint32_t
int64_t / uint64_t
性能上原子操作的速度是互斥锁的6~7倍。

有了这些函数, 就可以很方便的进行原子操作了, 以 count++ 为例, 

count 初始值为0, 可以这么写

__sync_fetch_and_add(&count, 1);//返回0, count现在等于1, 类似 count ++

count 初始值为0, 或者这么写

__sync_add_and_fetch(&count, 1);//返回1, count现在等于1, 类似 ++ count


原子操作也可以用来实现互斥锁:

int a = 0;
#define LOCK(a) while (__sync_lock_test_and_set(&a,1)) {sched_yield();}
#define UNLOCK(a) __sync_lock_release(&a);

sched_yield()这个函数可以使用另一个级别等于或高于当前线程的线程先运行。如果没有符合条件的线程,那么这个函数将会立刻返回然后继续执行当前线程的程序。  

如果去掉 
sched_yield(), 这个锁就会一直自旋.

下面我们利用原子操作来实现一个无锁并发堆栈;
struct Node{
    void* data;
    Node* next
    Node(void* d):data(d),next(NULL){} 
};

class Stack{
public:
    Stack():top(NULL){}
    void Push(void* d);
    void* Pop();
private:
    Node *top;
};


void Stack::Push(void* d){
    Node* n = new Node(d);
    for (;;){
        n->next = top;
        if (__sync_bool_compare_and_swap(&top, n->next, n)){
            break;
        }
    }
}


压栈操作首先创建了一个新节点,它的 next 指针指向堆栈的顶部。然后用原子操作把新的节点复制到 top 位置。 从多个线程的角度来看,完全可能有两个或更多线程同时试图把数据压入堆栈。假设线程 A 试图把 pA 压入堆栈,线程 B 试图压入 pB,线程 A 先获得了时间片。在 
n->next = top
 指令结束之后,调度程序暂停了线程 A。现在,线程 B 获得了时间片,它能够完成原子操作,把 pB 压入堆栈后结束。接下来,线程 A 恢复执行,显然对于这个线程 
*top
 和 
n->next
 不匹配,因为线程 B 修改了 top 位置的内容。因此,代码回到循环的开头,指向正确的 top 指针(线程 B 修改后的),调用原子操作,把 pA 压入堆栈后结束。



void* Stack::Pop(){
    for (;;){
        Node* n = top;
        if (n == NULL){
            return NULL;
        }
        if (top != NULL && __sync_bool_compare_and_swap(&top, n, n->next)){
            void* p = n->data;
            delete n;
            return p;
        }
    }
}

出栈操作的原理和压栈类似. 即使线程 B 在线程 A 试图弹出数据的同时修改了堆栈顶,也可以确保不会跳过堆栈中的元素。


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/140637.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 两个向量的点乘和叉乘怎么算_数学基础 —— 向量运算:点乘和叉乘

    两个向量的点乘和叉乘怎么算_数学基础 —— 向量运算:点乘和叉乘向量的点乘:a*b公式:a*b=|a|*|b|*cosθ点乘又叫向量的内积、数量积,是一个向量和它在另一个向量上的投影的长度的乘积;是标量。点乘反映着两个向量的“相似度”,两个向量越“相似”,它们的点乘越大。向量的叉乘:a∧ba∧b=|a|*|b|*sinθ向量积被定义为:模长:(在这里θ表示两向量之间的夹角(共起点的前提下)(0°≤θ≤180°),…

    2025年6月8日
    1
  • centos7 安装 nginx[通俗易懂]

    centos7 安装 nginx[通俗易懂]一、安装所需插件1、安装gccgcc是linux下的编译器,它可以编译C,C++,Ada,ObjectC和Java等语言。yum-yinstallgcc2、安装pcre、pcre-develpcre是一个perl库,包括perl兼容的正则表达式库,nginx的http模块使用pcre来解析正则表达式。yuminstall-ypcrepcre-devel3、zlib安装zlib库提供了很多种压缩和解压缩方式nginx使用zlib对http包的内容进行gz

    2022年5月12日
    42
  • base64编码图片数据存储服务器

    base64编码图片数据存储服务器如果直接提交base64编码图片数据,过大的话后台会出现转发错误问题。我在刚开始接触base64编码图片数据时,就是把base64编码图片数据传到后台来解码生成图片。导致生成的图片无法打开,后来才发现其实传到后台的base64编码根本就不完整,导致解码出现问题,无法显示图片。所以,base64编码只能在前端处理。后来查阅资料,看见一个不错的解决方式就是

    2022年4月13日
    50
  • NC65 自由报表开发「建议收藏」

    NC65 自由报表开发「建议收藏」动态建模平台—->报表平台如果找不到?则登录账套管理员分配集团管理员的权限可参考下面链接https://blog.csdn.net/qq_19004705/article/details/119889910概述自由报表:是可利用报表分析工具设计出固定格式的、具有强大分析功能的分析型报表,可对报表数据进行各种自由分析。提供对数据集的复杂分析类设计功能,得到可适应企业决策人员使用的分析型报表及报表数据;同时也提供对已存在业务系统数据、采集报表数据,通过数据集进行随意组合查..

    2022年8月30日
    4
  • 排队论[通俗易懂]

    排队论[通俗易懂]排队论简介历史排队论又称随机服务系统,是研究系统随机聚散现象和随机服务系统工作过程的数学理论和方法,是运筹学的一个分支。排队论的基本思想是1909年丹麦数学家A.K.埃尔朗在解决自动

    2022年8月5日
    5
  • 生命游戏程序_生命游戏怎么玩

    生命游戏程序_生命游戏怎么玩引言群居性昆虫是一个生命,鱼群、鸟群是一个生命,社会、城市是一个有机体,人类的语言是活的,人类的集体行为也是活的。这些复杂系统是如何设计出来的?世界上最著名的游戏之一,GameofLife生命游戏,为这些最神秘的问题提出了可能的解释——也许再复杂的生命,最初也不过是几条最简单的规则。本文从GameofLife的缘起说起,解释了它这几十年给予数学、计算机、哲学的启发,最后把它作为P…

    2022年10月17日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号