多线程原子操作方法(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

相关文章

Python基础文件操作方法超详细讲解(详解版)

《Python基础文件操作方法超详细讲解(详解版)》文件就是操作系统为用户或应用程序提供的一个读写硬盘的虚拟单位,文件的核心操作就是读和写,:本文主要介绍Python基础文件操作方法超详细讲解的相... 目录一、文件操作1. 文件打开与关闭1.1 打开文件1.2 关闭文件2. 访问模式及说明二、文件读写1.

Java使用多线程处理未知任务数的方案介绍

《Java使用多线程处理未知任务数的方案介绍》这篇文章主要为大家详细介绍了Java如何使用多线程实现处理未知任务数,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 知道任务个数,你可以定义好线程数规则,生成线程数去跑代码说明:1.虚拟线程池:使用 Executors.newVir

python中的整除向下取整的操作方法

《python中的整除向下取整的操作方法》Python中的//是整数除法运算符,用于执行向下取整的除法,返回商的整数部分,不会四舍五入,它在分治法、索引计算和整数运算中非常有用,本文给大家介绍pyth... 目录1. // 的基本用法2. // vs /(普通除法)3. // 在 mid = len(lis

JAVA封装多线程实现的方式及原理

《JAVA封装多线程实现的方式及原理》:本文主要介绍Java中封装多线程的原理和常见方式,通过封装可以简化多线程的使用,提高安全性,并增强代码的可维护性和可扩展性,需要的朋友可以参考下... 目录前言一、封装的目标二、常见的封装方式及原理总结前言在 Java 中,封装多线程的原理主要围绕着将多线程相关的操

MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固 通俗易懂版)

《MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固通俗易懂版)》本文主要讲解了MySQL中的多表查询,包括子查询、笛卡尔积、自连接、多表查询的实现方法以及多列子查询等,通过实际例子和操... 目录复合查询1. 回顾查询基本操作group by 分组having1. 显示部门号为10的部门名,员

通过ibd文件恢复MySql数据的操作方法

《通过ibd文件恢复MySql数据的操作方法》文章介绍通过.ibd文件恢复MySQL数据的过程,包括知道表结构和不知道表结构两种情况,对于知道表结构的情况,可以直接将.ibd文件复制到新的数据库目录并... 目录第一种情况:知道表结构第二种情况:不知道表结构总结今天干了一件大事,安装1Panel导致原来服务

Javascript访问Promise对象返回值的操作方法

《Javascript访问Promise对象返回值的操作方法》这篇文章介绍了如何在JavaScript中使用Promise对象来处理异步操作,通过使用fetch()方法和Promise对象,我们可以从... 目录在Javascript中,什么是Promise1- then() 链式操作2- 在之后的代码中使

Spring AI集成DeepSeek实现流式输出的操作方法

《SpringAI集成DeepSeek实现流式输出的操作方法》本文介绍了如何在SpringBoot中使用Sse(Server-SentEvents)技术实现流式输出,后端使用SpringMVC中的S... 目录一、后端代码二、前端代码三、运行项目小天有话说题外话参考资料前面一篇文章我们实现了《Spring

Python中多线程和多进程的基本用法详解

《Python中多线程和多进程的基本用法详解》这篇文章介绍了Python中多线程和多进程的相关知识,包括并发编程的优势,多线程和多进程的概念、适用场景、示例代码,线程池和进程池的使用,以及如何选择合适... 目录引言一、并发编程的主要优势二、python的多线程(Threading)1. 什么是多线程?2.

Linux使用cut进行文本提取的操作方法

《Linux使用cut进行文本提取的操作方法》Linux中的cut命令是一个命令行实用程序,用于从文件或标准输入中提取文本行的部分,本文给大家介绍了Linux使用cut进行文本提取的操作方法,文中有详... 目录简介基础语法常用选项范围选择示例用法-f:字段选择-d:分隔符-c:字符选择-b:字节选择--c