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

时间:2016-08-02 23:54:09

标签: c++ linux multithreading cpu

我有以下简单的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个thread1绑定到核心5运行,所以从理论上讲,所有3个thread1中只有一个 可以在核心5运行,因为iglbcnt是对所有thread1的全局变量, 我希望答案是3亿,不幸的是它不是所有的时间 获得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

Edit2:

for(int idx=0;idx<CNTNUM;idx++){
    asm volatile("":::"memory") ;
    iglbcnt++ ;
}

然后通过-O1编译它将正常工作, 在这种情况下,添加编译器时间内存屏障会有所帮助。

2 个答案:

答案 0 :(得分:2)

igblcnt ++是一个加载,添加,存储序列。这是在没有同步的情况下执行的,因此线程(即使在同一个核心上进行调度)也会有竞争,因为每个线程都有自己的寄存器上下文。 igblcnt上的__sync_add_and_fetch指令将解决竞争问题。

进入核心寄存器的负载发生然后线程被切换(它的寄存器被保存)另一个线程读取相同的值并递增并将其存储回存储器(可能是数百个增量)然后第一个线程被切换与其陈旧的值,增加和存储 - 潜在地减少数千到数百万的增量(如您所见)。

答案 1 :(得分:0)

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

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