运行 在单个内核上的多个线程如何进行数据竞争?

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 会解决问题, 但我还是想知道导致这个结果的细节。

编辑:

++igblcntigblcnt++ 生成相同的代码。

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 指令将解决竞争。

加载到核心的寄存器中,然后线程被切换(它的寄存器被保存)另一个线程读取相同的值并递增并将其存储回内存(可能是数百个递增)然后第一个线程以其递增和存储的陈旧值切换 - 可能会损失数千到数百万的增量(如您所见)。

如果一个处理器上的线程 运行 被抢先调度,则它们可能会发生数据竞争,这意味着中断可能随时发生,从而触发线程上下文切换。然后线程必须使用互斥机制,如互斥对象,或者原子指令(连同精心设计的算法)。

单个处理器上的协作调度线程隐式避免了数据竞争。在单个处理器上的协作线程下,一个线程一直执行到它显式调用某个切换上下文的函数为止。任何不调用此类函数的代码都不会受到其他线程的干扰。