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

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

编辑:

++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 有一场比赛。所有线程都在读写这个变量。 增量是一个读-修改-写操作。如果两个线程读取相同的值,它们都将写回相同的增量值——丢失一个增量。你需要一个互锁的增量——对于 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】:

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

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

【讨论】:

以上是关于在一个内核上运行的多个线程如何进行数据竞争?的主要内容,如果未能解决你的问题,请参考以下文章

多线程如何利用多个内核?

如何确定 Java 线程在哪个内核上运行?

单核CPU如何执行多线程

Java多线程基础

第九章:内核同步介绍

如何使此 Java 代码正常运行? [多线程,竞争条件]