运行 在单个内核上的多个线程如何进行数据竞争?
How can multiple threads, running on a single core, have a data race?
我有以下简单的 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 个线程中只有一个
可以 运行ning 在 core 5 ,并且由于 iglbcnt 是所有 thread1 的全局变量,
我希望答案是 300000000 ,不幸的是它并非总是如此
得到 300000000 ,有时会像 292065873 一样出来。
我想为什么不总是得到 300000000 的原因是在核心 5 中进行上下文切换时,iglbcnt 的值仍然保留在 cpu 的存储缓冲区中,所以当调度程序 运行 另一个thread 那么 L1 缓存中 iglbcnt 的值将是
与 cpu 核心 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++ 是加载、添加、存储序列。这是在没有同步的情况下执行的,因此线程(即使调度在同一个内核上)也会发生竞争,因为它们每个人都有自己的寄存器上下文。 igblcnt 上的 __sync_add_and_fetch 指令将解决竞争。
加载到核心的寄存器中,然后线程被切换(它的寄存器被保存)另一个线程读取相同的值并递增并将其存储回内存(可能是数百个递增)然后第一个线程以其递增和存储的陈旧值切换 - 可能会损失数千到数百万的增量(如您所见)。
如果一个处理器上的线程 运行 被抢先调度,则它们可能会发生数据竞争,这意味着中断可能随时发生,从而触发线程上下文切换。然后线程必须使用互斥机制,如互斥对象,或者原子指令(连同精心设计的算法)。
单个处理器上的协作调度线程隐式避免了数据竞争。在单个处理器上的协作线程下,一个线程一直执行到它显式调用某个切换上下文的函数为止。任何不调用此类函数的代码都不会受到其他线程的干扰。
我有以下简单的 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 个线程中只有一个 可以 运行ning 在 core 5 ,并且由于 iglbcnt 是所有 thread1 的全局变量, 我希望答案是 300000000 ,不幸的是它并非总是如此 得到 300000000 ,有时会像 292065873 一样出来。
我想为什么不总是得到 300000000 的原因是在核心 5 中进行上下文切换时,iglbcnt 的值仍然保留在 cpu 的存储缓冲区中,所以当调度程序 运行 另一个thread 那么 L1 缓存中 iglbcnt 的值将是 与 cpu 核心 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++ 是加载、添加、存储序列。这是在没有同步的情况下执行的,因此线程(即使调度在同一个内核上)也会发生竞争,因为它们每个人都有自己的寄存器上下文。 igblcnt 上的 __sync_add_and_fetch 指令将解决竞争。
加载到核心的寄存器中,然后线程被切换(它的寄存器被保存)另一个线程读取相同的值并递增并将其存储回内存(可能是数百个递增)然后第一个线程以其递增和存储的陈旧值切换 - 可能会损失数千到数百万的增量(如您所见)。
如果一个处理器上的线程 运行 被抢先调度,则它们可能会发生数据竞争,这意味着中断可能随时发生,从而触发线程上下文切换。然后线程必须使用互斥机制,如互斥对象,或者原子指令(连同精心设计的算法)。
单个处理器上的协作调度线程隐式避免了数据竞争。在单个处理器上的协作线程下,一个线程一直执行到它显式调用某个切换上下文的函数为止。任何不调用此类函数的代码都不会受到其他线程的干扰。