在一个内核上运行的多个线程如何进行数据竞争?
Posted
技术标签:
【中文标题】在一个内核上运行的多个线程如何进行数据竞争?【英文标题】:How can multiple threads, running on a single core, have a data race? 【发布时间】:2016-08-02 23:54:09 【问题描述】:我有以下简单的 c++ 源代码:
#define CNTNUM 100000000
int iglbcnt = 0 ;
int iThreadDone = 0 ;
void *thread1(void *param)
/*
pid_t tid = syscall(SYS_gettid);
cpu_set_t set;
CPU_ZERO( &set );
CPU_SET( 5, &set );
if (sched_setaffinity( tid, sizeof( cpu_set_t ), &set ))
printf( "sched_setaffinity error" );
*/
pthread_detach(pthread_self());
for(int idx=0;idx<CNTNUM;idx++)
iglbcnt++ ;
printf(" thread1 out \n") ;
__sync_add_and_fetch(&iThreadDone,1) ;
int main(int argc, char **argv)
pthread_t tid ;
pthread_create(&tid , NULL, thread1, (void*)(long)1);
pthread_create(&tid , NULL, thread1, (void*)(long)3);
pthread_create(&tid , NULL, thread1, (void*)(long)5);
while( 1 )
sleep( 2 ) ;
if( iThreadDone >= 3 )
printf("iglbcnt=(%d) \n",iglbcnt) ;
如果我运行它,答案肯定不会是 300000000,除非源使用 __sync_add_and_fetch(iglbcnt, 1 ) 而不是 iglbcnt++。
然后我尝试像 numactl -C 5 ./x.exe 一样运行,numactl 尝试亲和所有 3 个线程 1 以在核心 5 上运行,所以理论上,所有 3 个线程 1 中只有一个 可以在核心 5 上运行,并且由于 iglbcnt 是所有 thread1 的全局变量, 我希望答案是 300000000 ,不幸的是并非一直如此 得到 300000000 ,有时像 292065873 一样出来。
我猜为什么不总是得到 300000000 的原因是在核心 5 中进行上下文切换时, iglbcnt 的值仍然保留在 cpu 的存储缓冲区中,所以当调度程序运行另一个线程时,L1 缓存中 iglbcnt 的值将是 与 cpu core 5 的存储缓冲区中的值不同,导致答案 来 292065873 ,而不是 300000000 。
这只是实验,正如我所说的 __sync_add_and_fetch 将解决问题, 但我仍然想知道导致这个结果的细节。
编辑:
++igblcnt
和 igblcnt++
生成相同的代码。
g++ --std=c++11 -S -masm=intel x.cpp ,(source ++iglbcnt) 以下代码来自 x.s :
.LFB11:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 32
mov QWORD PTR [rbp-24], rdi
call pthread_self
mov rdi, rax
call pthread_detach
mov DWORD PTR [rbp-4], 0
jmp .L2
.L3:
mov eax, DWORD PTR iglbcnt[rip]
add eax, 1
mov DWORD PTR iglbcnt[rip], eax
add DWORD PTR [rbp-4], 1
.L2:
cmp DWORD PTR [rbp-4], 99999999
jle .L3
mov edi, OFFSET FLAT:.LC0
call puts
lock add DWORD PTR iThreadDone[rip], 1
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE11:
.size _Z7thread1Pv, .-_Z7thread1Pv
.section .rodata
.LC1:
.string "iglbcnt=(%d) \n"
.text
编辑2:
for(int idx=0;idx<CNTNUM;idx++)
asm volatile("":::"memory") ;
iglbcnt++ ;
然后用 -O1 编译就可以了, 在这种情况下,添加编译器时内存屏障会有所帮助。
【问题讨论】:
你在 ++igblcnt 有一场比赛。所有线程都在读写这个变量。 增量是一个读-修改-写操作。如果两个线程读取相同的值,它们都将写回相同的增量值——丢失一个增量。你需要一个互锁的增量——对于 gcc 来说是 __sync_add_and_fetch。 @David,是的,使用 gcc 4.8.2 我可以问什么 CPU? @David ,我知道你的意思,我的问题是我亲和所有线程都运行在 cpu core5 中,所以这三个线程中只有一个可以执行 iglbcnt++ ,我猜这个结果的核心原因来自cpu的设计,我猜是和store buffer有关吧。 【参考方案1】:igblcnt++ 是一个加载、添加、存储序列。这是在没有同步的情况下执行的,因此线程(即使在同一个内核上调度)将有竞争,因为它们每个都有自己的寄存器上下文。 igblcnt 上的 __sync_add_and_fetch 指令将解决竞争。
加载到内核的寄存器中,然后线程被切换出去(它的寄存器被保存)另一个线程读取相同的值并递增并将其存储回内存(可能是数百个递增),然后切换第一个线程其陈旧的价值会增加和存储 - 可能会损失数千到数百万的增量(如您所见)。
【讨论】:
跟store buffer没关系?! 感谢您的回复,您能否详细解释一下 cpu register context 中发生的事情,有关 context switch 的一些信息?!【参考方案2】:如果在一个处理器上运行的线程被抢先调度,它们可能会发生数据竞争,这意味着任何时候都可能发生中断,从而触发线程上下文切换。然后线程必须使用互斥机制,如互斥对象或原子指令(以及精心设计的算法)。
单个处理器上的协作调度线程隐式地避免了数据竞争。在单个处理器上的协作线程下,一个线程一直执行,直到它显式调用某个切换上下文的函数。任何不调用此类函数的代码都不会受到其他线程的干扰。
【讨论】:
以上是关于在一个内核上运行的多个线程如何进行数据竞争?的主要内容,如果未能解决你的问题,请参考以下文章