多线程原子操作方法(atomic,compare_and_swap)

2024-04-12 02:32

本文主要是介绍多线程原子操作方法(atomic,compare_and_swap),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在前面的多处理器编程艺术文章中,谈到了lock free算法。多线程编程通常碰到的问题是很多操作的非原子性(non-atomic)。
本文通过程序实例,说明多线程中由于访问同一数据变量可能造成写丢失的情况。并通过mutex、atomic、compare_and_swap逻辑完成对于多线程的控制。
1.非线程安全方式
多个线程并发地将一个变量相加n次。

volatile unsigned long counter = 0;void
Add(void *arg)
{int i;int iter = * ((int*)arg);for (i = 0; i < iter; i++){++counter;   //非线程安全的}return NULL;
}int
main(int argc, char *argv[]) 
{int i, num, count;pthread_t *t;if (argc != 3){fprintf(stderr, "usage %s: <nthreads> <iterations>\n", argv[0]);return 1;}num = atoi(argv[1]);count = atoi(argv[2]);/* create threads */t = (pthread_t *) malloc(num * sizeof(pthread_t));for (i = 0; i < num; i++){assert(!pthread_create(&t[i], NULL, Add, (void *) &count));}for (i = 0; i < num; i++){assert(!pthread_join(t[i], NULL));}printf("all thread done, the result is:%lu\n", counter);}

其中,++counter语句为非线程安全的。我们通过反汇编查看具体的操作步骤:(gdb -q ./unsafe_add ,输入dsias Add查看结果)

8048540 <Add+16>:	mov    0x8049988,%eax
0x08048545 <Add+21>:	add    $0x1,%edx
0x08048548 <Add+24>:	add    $0x1,%eax
0x0804854b <Add+27>:	cmp    %ecx,%edx
0x0804854d <Add+29>:	mov    %eax,0x8049988


其中可以看出++counter实际上按照三条语句执行。
a.先将内存中的变量保存在eax寄存器中;
b.将寄存器内容+1;
c.将寄存器中的内容写回内存;
多线程运行时,可能出现交差的情况,如下所示:
MOV counter,%rax thread-0
MOV counter,%rax thread-1
ADD 0x01,%rax thread-0
ADD 0x01,%rax thread-1
MOV %rax,counter thread-0
MOV %rax,counter thread-1
此时多个线程执行,会将部分的执行结果覆盖,所以,创建5个线程,各增加100000次,可能出现的最后结果为:
all thread done, the result is:268565
2.互斥锁方式
通过互斥访问的方法,在同一时刻只能有一个线程进入临界区进行操作。
采用pthread库,使用mutex对象控制各线程对于counter变量的访问。

void
Add(void *arg)
{int i;int iter = * ((int*)arg);for (i = 0; i < iter; i++){pthread_mutex_lock(&mutex);++counter;  //critical sectionpthread_mutex_unlock(&mutex);}return NULL;
}

同一时刻,只能由一个线程改变counter的值。
注:此时counter要声明为volatile,不然可能会被有的编译器"虐".可以参加该链接。
http://concurrencykit.org/doc/concurrencyPrimitives.html#3.1
3. 原子操作方式
现代的CPU支持原子操作的指令。比如:xadd,配合lock指令,完成fetch_and_add操作。可以参见:
http://lists.cs.uiuc.edu/pipermail/llvmdev/2004-December/002793.html
http://en.wikipedia.org/wiki/Fetch-and-add#x86_implementation
我们通过gcc内联汇编的方式实现原子增加方法:

static inline unsigned int AtomicAdd(volatile unsigned long *mem, unsigned long add)
{unsigned long __tmp = add;__asm__ __volatile__("lock xaddq %0,%1":"+r" (add),"+m" (*mem): : "memory");return add + __tmp;
}

4. GCC Atomic Builtins方式
gcc从4.1版本开始提供了原子操作。可以参见:
http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html
我们采用__sync_fetch_and_add方式。

void
Add(void *arg)
{int i;int iter = * ((int*)arg);for (i = 0; i < iter; i++){__sync_fetch_and_add(&counter, 1);}return NULL;
}

5.lock free方式
现代cpu主要通过cmpxchg指令来达到lockfree的效果,也就是compare_and_swap机制。可以参见:
http://en.wikipedia.org/wiki/Non-blocking_algorithm
http://en.wikipedia.org/wiki/Compare-and-swap
compare_and_swap机制逻辑如下:

/* atomic */
bool compare_and_swap (unsigned long *mem, unsigned long old, unsigned long new)
{if (*mem == old) {*mem = newreturn true;}return false;
}

比较内存mem中的值是否与给定的old值一致时,用new值替换mem中的数据。考虑++counter语句,线程将counter进行拷贝,增加后再更新counter,如果通过cas比较counter的值与拷贝的值不能,则说明有其他线程已经更新了counter,需要重新load,否则更新counter。

static inline char CAS(volatile unsigned long *mem, unsigned long old, unsigned long new)
{unsigned long r;__asm__ __volatile__("lock cmpxchg %k2, %1": "=a" (r), "+m" (*mem): "r" (new), "0" (old): "memory");return r == old ? 1 : 0;
}

6.测试
编写如下脚本,对于不同的方式执行时间进行测试:

for pg in atomic_add gcc_add mutex_add cas_add; do
#for pg in  mutex_add; dofor (( i=1; $i <= 10; ++i)); dolet nops=1000000/$iecho -n "$i $nops"time  ./$pg $i $nops 1> /dev/nulldone
done

可以看出mutex方式由于涉及到线程的切换,所花费的时间代价很大。

相关的代码为:
lockfree.tar
注:其中的xadd、cmpxchg指令是在64位主机下使用的,如果在32位主机下,可能需要使用xaddl、cmpxchgl指令。

http://www.financecomputing.net/wordpress/?p=245

这篇关于多线程原子操作方法(atomic,compare_and_swap)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/895861

相关文章

Kafka拦截器的神奇操作方法

《Kafka拦截器的神奇操作方法》Kafka拦截器是一种强大的机制,用于在消息发送和接收过程中插入自定义逻辑,它们可以用于消息定制、日志记录、监控、业务逻辑集成、性能统计和异常处理等,本文介绍Kafk... 目录前言拦截器的基本概念Kafka 拦截器的定义和基本原理:拦截器是 Kafka 消息传递的不可或缺

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

使用SQL语言查询多个Excel表格的操作方法

《使用SQL语言查询多个Excel表格的操作方法》本文介绍了如何使用SQL语言查询多个Excel表格,通过将所有Excel表格放入一个.xlsx文件中,并使用pandas和pandasql库进行读取和... 目录如何用SQL语言查询多个Excel表格如何使用sql查询excel内容1. 简介2. 实现思路3

使用Python处理CSV和Excel文件的操作方法

《使用Python处理CSV和Excel文件的操作方法》在数据分析、自动化和日常开发中,CSV和Excel文件是非常常见的数据存储格式,ython提供了强大的工具来读取、编辑和保存这两种文件,满足从基... 目录1. CSV 文件概述和处理方法1.1 CSV 文件格式的基本介绍1.2 使用 python 内

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

多线程解析报表

假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。 Way1 join import java.time.LocalTime;public class Main {public static void main(String[] args) thro

Java 多线程概述

多线程技术概述   1.线程与进程 进程:内存中运行的应用程序,每个进程都拥有一个独立的内存空间。线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换、并发执行,一个进程最少有一个线程,线程实际数是在进程基础之上的进一步划分,一个进程启动之后,进程之中的若干执行路径又可以划分成若干个线程 2.线程的调度 分时调度:所有线程轮流使用CPU的使用权,平均分配时间抢占式调度

Java 多线程的基本方式

Java 多线程的基本方式 基础实现两种方式: 通过实现Callable 接口方式(可得到返回值):

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因