线程同步问题

时间:2015-10-25 16:11:04

标签: c++ multithreading

在下面的例子中,我最后为两个线程调用pthread_join()(在打印总和之前)。即使预期总和应为0,它也会打印任何值。我知道如果我在创建第二个线程之前pthread_join(id1,NULL)那么它会正常工作(确实如此),但我不明白为什么当我为两个线程调用join时它不能工作结束。

因为只有在两个线程都必须完全执行完毕后才会打印 sum 。因此,在执行第一个线程之后,它必须将2000000添加到变量sum中,并且第二个线程必须从总和中减去2000000,应该是0

long long sum=0;

void* counting_thread(void* arg)
{   
    int offset = *(int*) arg;
    for(int i=0;i<2000000;i++)
    {
        sum=sum+offset;
    }
    pthread_exit(NULL);
}
int main(void)
{
    pthread_t id1;
    int offset1 = 1;
    pthread_create(&id1,NULL,counting_thread,&offset1);

    pthread_t id2;
    int offset2 = -1; 
    pthread_create(&id2,NULL,counting_thread,&offset2);

    pthread_join(id1,NULL);
    pthread_join(id2,NULL);

    cout<<sum;
}

3 个答案:

答案 0 :(得分:5)

问题是sum=sum+offset;不是线程安全的。

这导致一些金额不计算在内。

正如您指定的C ++,std::atomic<long long> sum;会有所帮助,但您需要使用+=运算符,而不是线程不安全sum = sum + count;

sum += offset;

阻止更新的互斥锁也会有所帮助。

如果没有这些更改,编译器可以生成代码,

  • 在函数开头读取sum,只有一个线程应用其更改。
  • 添加的陈旧值为sum
  • 缓存中的状态不正确。

读取优化

编译器可以在线程启动时合法地读取sum的值,向其添加n次n,并存储该值。这意味着只有一个线程可以工作。

陈旧价值

考虑以下程序集代码。

read sum
add offset to sum
store sum

thread1                     thread2
1 read sum                     
2 add offset to sum            read sum
3 store sum                    add offset to sum
4 read sum                     store sum
5 add offset to sum            read sum
6 store sum                    add offset to sum

线程2的第3行将偏移量添加到旧值,这使得线程1的第3行丢失。

缓存中的状态不正确

在多线程系统中,缓存可能在进程的线程之间不一致。

这意味着即使在执行sum+=offset之后,另一个核心/ CPU也可能会看到预先更新的值。

这使得CPU可以更快地运行,因为它们可以忽略在它们之间共享数据。但是,当2个线程访问相同的数据时,需要将其考虑在内。

std::atomic / mutex确保: -

  1. 该值以原子方式修改(就好像sum = sum + count不可分割一样)。
  2. 这些值在核心/ CPU之间始终可见。
  3. 编译器不会对sum的加载/存储进行重新排序,就好像它无法更改一样。

答案 1 :(得分:5)

您可以在没有同步的情况下获得任何结果,因为add操作不是 atomic

在基础层面

你的

sum=sum+offset;

实际上是

fetch sum to register # tmp := sum
add offset            # tmp := tmp + offset
store new value       # sum := tmp

现在想象两个线程同时工作

Thread1     Thread2    Sum
tmp:= 1     tmp:=1     1
tmp:= 1+1   tmp:=1-1   1
-zzz-       sum := 0   0
sum := 2     -zzz-     2

在这个计算的serai中,线程2减法的结果丢失了

如果我稍微改变时间

Thread1     Thread2    Sum
sum := 2     -zzz-     2
-zzz-       sum := 0   0

我将丢失线程1添加

添加一些优化器

现在情况变得更糟。如果你不同步,编译器会认为不会发生任何事情(因为编译器总是信任你)

因此它将跳过获取和存储部分并将代码转换为

fetch sum to register # tmp := sum
add offset N times    # for (i := 1 ; i < 2000000; i++) tmp := tmp + offset
store result          # sum := tmp

甚至

fetch sum to register # tmp := sum
add offset * N        # tmp := tmp + 2000000 * offset
sore tmp              # sum := tmp 

现在想象两个线程在这里同时工作

添加一些与机器相关的行为

之前已经介绍了基本思想,但不仅仅是编译器可以归咎于你的平台本身。缓存机制允许更快的数据访问,但如果缓存未被同步,则不同的线程可以读取相同变量的不同值

答案 2 :(得分:2)

同时修改全局变量sum的两个线程之间没有同步。您需要在代码周围使用互斥锁,或者您需要使用平台提供的原子递增/递减功能之一。

如果无法正确同步线程,则此代码会遇到“丢失更新”问题。请参阅此链接,了解Oracle术语线程干扰。 https://docs.oracle.com/javase/tutorial/essential/concurrency/interfere.html他们谈论的是Java,但C / C ++也是如此。 sum = sum + offset不是原子操作。大多数平台都有自动更新变量的操作,例如Windows上的InterlockedIncrement和Linux上的_sync_add_and_fetch()。

编辑:这个程序也在Anthony Williams的文章“Avoiding the Perils of C++0x Data Races”中进行了详细研究。