多线程的数据安全问题

六道佩恩 2020-07-06 07:47:28
a是全局变量,初值为0
两条线程循环执行
{
a++;
a--;
}
最后的结果非0
奇怪的是,当a++在前时,结果总是正的
a--在前时,结果总是负的

a++和a--都只有一条汇编码
即便线程切换,a++和a--执行的次数应该都是不变的,最后应该回到0才对呀

而且交换a++和a--的顺序所造成的怪异现象又如何解释?
值的修改都是直接在内存中进行的,没有经过寄存器

我目前能想到的原因,执行a--后,数据还没修改时,已经切换了线程并执行了a++,然后前面的a--作废

哪位大佬给个真相?
...全文
726 24 打赏 收藏 转发到动态 举报
写回复
用AI写文章
24 条回复
切换为时间正序
请发表友善的回复…
发表回复
六道佩恩 2020-07-13
  • 打赏
  • 举报
回复
引用 23 楼 薛定谔之死猫 的回复:
按照楼主的描述,如果代码块没做同步,是不可能得到理想状态的0的,在汇编层级上,a++比++a还多一条指令。百思不得其解的时候可能是犯了低级错误了,主线程是不是没有join子线程?这样的话主线程带了runtime提供的退出进程代码,不管其它线程什么状态都一律通杀,代码执行是不可能完整的
我前面都贴了汇编码了,都是一条指令,a++和++a包括a+=1都被生成了同一条指令。 我有同步啊,不然哪来的结果 不过我又不是在探究同步、探究如何实现正确运算,我是在探究单条指令仍会出错的原因,因为我之前猜测的是线程切换,应该不会出现单条命令的运行错误,现在知道了,是多个处理器在同时运行,即两条线程在真正同时运行,不是切换造成的。
薛定谔之死猫 2020-07-12
  • 打赏
  • 举报
回复
按照楼主的描述,如果代码块没做同步,是不可能得到理想状态的0的,在汇编层级上,a++比++a还多一条指令。百思不得其解的时候可能是犯了低级错误了,主线程是不是没有join子线程?这样的话主线程带了runtime提供的退出进程代码,不管其它线程什么状态都一律通杀,代码执行是不可能完整的
六道佩恩 2020-07-11
  • 打赏
  • 举报
回复
引用 19 楼 早打大打打核战争 的回复:
凡是涉及内存访问的指令都不是原子操作(并非单指令就是原子操作),但是实现多线程条件下原子操作并非必须用lock前缀,使用lock前缀是轻量级、高性能,但是硬件依赖的方法。也可以用Semaphore、Multex、Critical Section之类的来实现一段代码串行化。
“凡是涉及内存访问的指令都不是原子操作”这句话是指所有情况,还是排除了lock等方案的情况?
mLee79 2020-07-11
  • 打赏
  • 举报
回复
lock 是比 CS PV 更基本的同步原语, 最基本的内核态的 SPINLOCK 也就关闭本核心任务调度, 循环 CAS 设置个标志位, 如果 lock 不是原子的, 根本就无法实现 自旋锁..
  • 打赏
  • 举报
回复
凡是涉及内存访问的指令都不是原子操作,所以为了原子化需要加lock前缀
使用Semaphore、Multex、Critical Section之类的来实现一段代码串行化,在串行化代码中访问共享变量相当于单线程操作,没有其他线程竞争,所以访问共享变量的指令不需要加lock前缀

  • 打赏
  • 举报
回复
凡是涉及内存访问的指令都不是原子操作(并非单指令就是原子操作),但是实现多线程条件下原子操作并非必须用lock前缀,使用lock前缀是轻量级、高性能,但是硬件依赖的方法。也可以用Semaphore、Multex、Critical Section之类的来实现一段代码串行化。
nice_cxf 2020-07-10
  • 打赏
  • 举报
回复
.file "test.c" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $0, -4(%rbp) addl $1, -4(%rbp) subl $1, -4(%rbp) nop popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)" .section .note.GNU-stack,"",@progbits 如果是整数数据,GCC就算不优化,也是单条汇编指令,VS就不行了,搞了3条。。。。
nice_cxf 2020-07-10
  • 打赏
  • 举报
回复
单行a++的话,如果a是整数,应该是原子操作吧?按说编译器应该能优化吧?不就是inc指令吗?
六道佩恩 2020-07-10
  • 打赏
  • 举报
回复
引用 15 楼 mLee79 的回复:
不管生成多少条指令, 各个核心操作的数据是自己的高速缓存, 如果不使用高速缓存一致性协议, 对同一个变量, 不同核心读到的同一个变量值本来就很可能不一致. 在 x86 上, 加 lock 前缀则使用高速缓存一致性协议通知其他核心刷新缓存状态, 在 ARM 上用 ldrex/strex, RISCV 上 lr/sc .. 大部分的U上, 只有写的时候需要通知其他核心缓存失效, 读的时候就随意啦 ...
如果没有lock是否就意为着不是原子操作?我看原子操作库函数生成的汇编码也没有lock,只有a++这类自增自减运算才有
  • 打赏
  • 举报
回复
如果多线程写同一个变量,肯定需要使用原子操作,如果多读一写,通常情况下不需要,除非该变量的位置跨越cache line
mLee79 2020-07-08
  • 打赏
  • 举报
回复
不管生成多少条指令, 各个核心操作的数据是自己的高速缓存, 如果不使用高速缓存一致性协议, 对同一个变量, 不同核心读到的同一个变量值本来就很可能不一致. 在 x86 上, 加 lock 前缀则使用高速缓存一致性协议通知其他核心刷新缓存状态, 在 ARM 上用 ldrex/strex, RISCV 上 lr/sc .. 大部分的U上, 只有写的时候需要通知其他核心缓存失效, 读的时候就随意啦 ...
六道佩恩 2020-07-08
  • 打赏
  • 举报
回复
引用 8 楼 月凉西厢 的回复:
a++、a--不是原子操作,不加锁肯定有问题啊。就算只有一条指令,当两个线程同时执行,也一样会有竞争问题
操作原子变量是否都应该使用原子操作库?如果汇编码中没有lock指令是否就意味着没有实现原子操作?
六道佩恩 2020-07-08
  • 打赏
  • 举报
回复
引用 2 楼 自信男孩 的回复:
没做好同步吧,线程执行线程体执行的先后以及次数是随机的。因此要么做好同步,要么确定好执行的先后
我是在探究问题出现的原因,而不是如何避免这类问题
六道佩恩 2020-07-08
  • 打赏
  • 举报
回复
引用 11 楼 早打大打打核战争 的回复:
如果多线程写同一个变量,肯定需要使用原子操作,如果多读一写,通常情况下不需要,除非该变量的位置跨越cache line
我之前说的就是原子类型,a++生成的汇编码有lock,但是a=1却没有,而且前者生成的汇编码很多,后者很少。关键是我试了C11的原子操作库来给变量赋值,生成的汇编码竟然也没有lock,这是不是说明实际上并没有达到原子的效果?
六道佩恩 2020-07-08
  • 打赏
  • 举报
回复
引用 9 楼 真相重于对错 的回复:
对变量a不使用a++而是使用a=a+1,我看汇编码里没有lock了,这时它还有原子操作的功能吗?是不是对于原子变量的操作都应该使用原子操作库?
Intel0011 2020-07-07
  • 打赏
  • 举报
回复
a++和a--都只有一条汇编码 ---> 这样理解是错误的,从汇编角度上是三条汇编码 实际上的例子 MOV EAX, [g_x] ; Thread 1: Move 0 into a register. INC EAX ; Thread 1: Increment the register to 1. MOV [g_x], EAX ; Thread 1: Store 1 back in g_x. MOV EAX, [g_x] ; Thread 2: Move 1 into a register. INC EAX ; Thread 2: Increment the register to 2. MOV [g_x], EAX ; Thread 2: Store 2 back in g_x. After both threads are done incrementing g_x, the value in g_x is 2. This is great and is exactly what we expect: take zero (0), increment it by 1 twice, and the answer is 2. Beautiful. But wait— Windows is a preemptive, multithreaded environment. So a thread can be switched away from at any time and another thread might continue executing at any time. So the preceding code might not execute exactly as I've written it. Instead, it might execute as follows: MOV EAX, [g_x] ; Thread 1: Move 0 into a register. INC EAX ; Thread 1: Increment the register to 1. MOV EAX, [g_x] ; Thread 2: Move 0 into a register. INC EAX ; Thread 2: Increment the register to 1. MOV [g_x], EAX ; Thread 2: Store 1 back in g_x. MOV [g_x], EAX ; Thread 1: Store 1 back in g_x. If the code executes this way, the final value in g_x is 1—not 2 as you expect! ---------- 这个是反汇编的结果 a++ mov ecx, [ebp+var_4] add ecx, 1 mov [ebp+var_4], ecx
自信男孩 2020-07-07
  • 打赏
  • 举报
回复
没做好同步吧,线程执行线程体执行的先后以及次数是随机的。因此要么做好同步,要么确定好执行的先后
真相重于对错 2020-07-07
  • 打赏
  • 举报
回复
#include <iostream>
#include <atomic>
#include <thread>
using namespace std;
atomic_llong a{ 0 };
void func1() {
	for (int i = 0; i < 100000; i++) {
		++a;
	}
}
void func2() {
	for (int i = 0; i < 100000; i++)
	{
		--a;
	}
}
int main()
{
	thread t1(func1);
	thread t2(func2);
	t1.join();
	t2.join();
	cout << a << endl;
}
qq_1457346882 2020-07-07
  • 打赏
  • 举报
回复
线程的话我记得好像是同时执行的,不加锁的情况拿到的就是原来的变量值而不是先改变后再拿到改变后的值
月凉西厢 2020-07-07
  • 打赏
  • 举报
回复
a++、a--不是原子操作,不加锁肯定有问题啊。就算只有一条指令,当两个线程同时执行,也一样会有竞争问题
加载更多回复(4)

69,371

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧