当前位置: 首页 > 知识库问答 >
问题:

在一个C程序中,使用两个并行线程将一个数字相加,然后减去回0,我得到了一个非零的答案。这似乎不可能

锺离良哲
2023-03-14

所讨论的计划是:

#include <pthread.h>
#include <stdio.h>

#define NUM_LOOPS 500000000
long long sum = 0;

/* Add or subtract the offset specified in arg from the sum NUM_LOOPS times */
void* counting_function(void* arg) {
    int offset = *(int*)arg;
    for (int i = 0; i < NUM_LOOPS; i++) {
        sum += offset;
    }
    pthread_exit(NULL);
}

int main(void) {
    // Spawn the threads
    pthread_t id1;
    int offset1 = 1;
    pthread_create(&id1, NULL, counting_function, &offset1);

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

    // Wait for threads to finish
    pthread_join(id1, NULL);
    pthread_join(id2, NULL);

    printf("Sum = %lld\n", sum);

    return 0;
}

我有一个全局变量long long sum=0和一个用于NUM_LOOPS500000000的宏,它指定要加起来的数字。

函数void*counting\u函数(void*arg)只是将偏移量添加到sumNUM\u循环次数。

如果我在没有线程的情况下运行它,但像这样一个接一个地调用函数

// sum is 0
counting_function(1);
// sum is now equal to 500000000
countnig_function(-1); 
// sum is now equal to 0

我会得到正确答案0

但当使用线程时,我会得到一个非零的答案,每次都不同。

gcc -Wall -Wextra -Werror -std=c99 -pthread count.c && ./a.out

Sum = 40098157
./a.out
Sum = 303575386

在我看来,无论你从一个数字中加或减1的顺序如何,如果加和减的总数是相同的,那么答案应该总是0。

例如,假设NUM\u LOOPS=10。第一个线程在一行中加1三次,然后第二个线程在一行中减去1两次。现在总和等于1。然后第一个线程进行剩余的七次加法,因此总和等于8,然后第二个线程减去最后八次。总和为0。

你怎么可能得到一个不同于0的答案?答案不同于0意味着两个线程中的一个在NUM_LOOPS中增加或减少的数量超过了指定的数量。

这个问题可以通过在计数函数内部的临界部分sum=偏移量中添加互斥来解决,但我似乎无法弄清楚为什么没有互斥锁它就无法工作。

我错过了什么?


共有3个答案

段良弼
2023-03-14

恭喜您尝试它!您已经发现尝试从两个线程更新相同的变量是有错误的。

您可以尝试一些解决方案。如果可能,您应该使用列表中较高的解决方案

  • 为每个线程指定一个单独的计数器,并在线程返回时将计数相加
  • 将计数设为原子变量。使用原子的fetch\U add\U explicit()递增和递减。在这种情况下,您可以使用memory\u order\u relaxed,因为更新的顺序无关紧要。在今天的大多数处理器上,它编译为一条无锁指令。如果必须确保在读取之前完成所有写入操作,则应改用获取释放内存顺序
  • 使用接收副本更新模式,在该模式中,他们尝试更新他们看到的共享日期的最后一个值,如果另一个线程在其间更改了该值,他们将重试。(此处非常不合适,但在许多其他情况下效果很好)
  • 让每个线程将其更改插入到无等待列表中,读取器线程将根据这些更改重建状态。(同样,这里有点荒谬,但总的来说值得考虑。)
  • 使用锁甚至读写器锁包装要更新的数据。(此处不需要,但有些资源需要。)
司徒宇
2023-03-14

pthread库将并行性硬塞进为单线程操作设计的语言中。我建议研究内存模型以及它们如何应用于多线程编程。

看看这里的线程和数据竞争部分。这里有一个与您的问题相关的示例。

这篇维基百科的文章有一个很好的章节介绍了种族状况。

有趣的是,这个问题早于pthread。您可以在带有信号处理程序的“单线程”C程序中观察到相同的行为,其中主程序和信号处理程序都在更新相同的变量。

刘德义
2023-03-14

当线程添加到sum时,它会分多个步骤执行:将sum加载到寄存器中。向寄存器添加值。将寄存器中的值存储到sum中。(这可能是两个步骤,例如在一个步骤中加载和添加,在另一个步骤中存储。)如果在此期间发生线程切换,您可以得到交错:线程1将sum加载到寄存器中,例如值400,并将1添加到寄存器中,得到401。有一个开关,线程2将仍然值为400的sum加载到寄存器中并添加−1,得到399。然后它存储它,所以sum是399。后来,线程1存储401。

因此,减量到399丢失;它被401的延迟存储覆盖。

当然,在切换回线程1之前,线程2可能会运行一段时间,因此线程2可能会有相当多的减量,这些减量会被线程1的延迟存储擦除。

可能发生的是随机选择这种交织,有效地从一个或另一个线程中擦除各种数量的加法。其中许多会取消——加法的一些擦除会取消减法的一些擦除,并且多次运行的结果将显示以零为中心的总和分布(如果一个线程先于另一个线程启动会扭曲分布,则可能接近于零)。

这只是多线程可能出现的一个问题。特别是由于sum是long-long,因此它可以在一些目标体系结构中使用多个机器字来实现,因此必须分多个步骤加载和存储。然后,多线程可能导致一个线程存储新值的一部分,另一个线程进入并修改sum,第一个线程完成对另一部分的存储。这可能产生比仅仅缺少增量或减量更具灾难性的结果;它会更严重地破坏sum的值。为了避免这种情况,您需要使用原子对象。

 类似资料: