【问题标题】:pthreads: If I increment a global from two different threads, can there be sync issues?pthreads:如果我从两个不同的线程增加一个全局,会不会有同步问题?
【发布时间】:2025-12-18 13:10:01
【问题描述】:

假设我有两个线程 A 和 B,它们都在递增 ~global~ 变量“count”。每个线程都运行一个类似这样的 for 循环:

for(int i=0; i<1000; i++)
    count++; //alternatively, count = count + 1;

即每个线程将 count 递增 1000 次,假设 count 从 0 开始。在这种情况下会出现同步问题吗?或者执行完成后是否正确计数等于 2000?我想既然语句“count = count + 1”可能会分解成两条汇编指令,那么另一个线程有可能在这两条指令之间交换吗?不确定。你怎么看?

【问题讨论】:

    标签: multithreading pthreads


    【解决方案1】:

    是的,在这种情况下可能会出现同步问题。您需要使用互斥锁保护计数变量,或者使用(通常是特定于平台的)原子操作。

    使用 pthread 互斥锁的示例

    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    for(int i=0; i<1000; i++) {
        pthread_mutex_lock(&mutex);
        count++;
        pthread_mutex_unlock(&mutex);
    }
    

    使用原子操作

    这里有关于平台特定原子操作的先前讨论: UNIX Portable Atomic Operations

    如果你只需要支持 GCC,这种方法很简单。如果您支持其他编译器,您可能需要根据平台做出一些决定。

    【讨论】:

      【解决方案2】:

      显然需要使用互斥锁或其他同步机制来保护计数。

      从根本上讲,count++ 语句分解为:

      load count into register
      increment register
      store count from register
      

      上下文切换可能发生在这些步骤之前/之后,导致以下情况:

      Thread 1:  load count into register A (value = 0)
      Thread 2:  load count into register B (value = 0)
      Thread 1:  increment register A (value = 1)
      Thread 1:  store count from register A (value = 1)
      Thread 2:  increment register B (value = 1)
      Thread 2:  store count from register B (value = 1)
      

      如您所见,两个线程都完成了一次循环迭代,但最终结果是 count 只增加了一次。

      您可能还想让 count volatile 以强制加载和存储进入内存,因为一个好的优化器可能会将 count 保存在寄存器中,除非另有说明。

      另外,我建议,如果这是要在您的线程中完成的所有工作,那么性能将因保持一致所需的所有互斥锁定/解锁而急剧下降。线程应该有更大的工作单元来执行。

      【讨论】:

      • Volatile 不会强制加载和存储进入内存,而不是以这里暗示的方式。 Volatile 让编译器知道,对于有问题的变量,它的值可以在编译器不知情的情况下发生变化,例如说一个实时时钟。这意味着编译器不会依赖该值的寄存器副本。然而,它并不能阻止 CPU 尽可能长时间地推迟将该高速缓存行写入内存(这就是它会做的事情,因为内存写入会破坏性能)。
      • 这个答案真的一点都不正确。一方面,绝对不能保证count++ 分解为三个操作的所有平台。它可能发生在某个平台上,也可能不会发生。答案从那里开始走下坡路,IMO。
      • @DavidSchwartz:好的,我会咬一口的。你会建议这里不需要同步吗?
      • 这里需要同步,因为 pthreads 标准没有定义当一个线程访问一个对象而另一个线程正在或可能正在修改它时会发生什么。因此,除非这是特定于平台的代码并且您的平台有一些特殊规则,否则您无法知道会发生什么。它甚至可能崩溃。
      • @DavidSchwartz 我不反对......我的例子只是为了展示事情可能出错的一种方式,作为激励我回应需要同步的一种方式。
      【解决方案3】:

      是的,可能存在同步问题。

      作为可能问题的示例,不能保证增量本身就是原子操作。

      换句话说,如果一个线程读取增量值然后被换出,另一个线程可以进来并更改它,那么第一个线程将写回错误的值:

      +-----+
      |   0 | Value stored in memory (0).
      +-----+
      |   0 | Thread 1 reads value into register (r1 = 0).
      +-----+
      |   0 | Thread 2 reads value into register (r2 = 0).
      +-----+
      |   1 | Thread 2 increments r2 and writes back.
      +-----+
      |   1 | Thread 1 increments r1 and writes back.
      +-----+
      

      所以你可以看到,即使两个线程都试图增加值,它也只增加了 1。

      这只是一个可能的问题。也可能是写入本身不是原子的,一个线程在被换出之前可能只更新部分值。

      如果您有保证在您的实现中有效的原子操作,您可以使用它们。否则,请使用互斥锁,这就是 pthreads 为同步提供的(并且保证会起作用),最安全的方法也是如此。

      【讨论】:

      • 您举例说明的问题在现代 CPU 上几乎没有意义。通常问题不在于以原子方式更新多个字节,因为机器通常一次以原子方式更新一个单词。此外,您所需要的只是一个 volatile 关键字。您在实践中通常看到的问题要简单得多:两个线程读取相同的值,每个线程在寄存器中独立递增,然后写回相同的值。
      • @blucz,我提供了一个更简单的情况(在您输入评论时正在进行中),但您大错特错。 POSIX 线程对它们正在运行的硬件以及它的写入是否是原子的做出假设。如果你要遵循一个标准,你应该遵循它。如果你想引入更多的假设,没关系,只要注意后果,不要假装遵循标准:-)
      • 我认为您误解了我的评论。我没有说任何关于 pthread 标准的事情。我只是建议,在实践中,你给出的例子不太可能,因为我给出的原因。有可能出现这种错误吗?当然......在我们 99.999% 的人永远不会看到的系统上。
      • 是的,不幸的是,我每天都工作这样一个野兽,但它几乎肯定负责跟踪进出您银行账户的每一分钱 :-) System z 大型机非常善于发现做出无根据假设的代码,比如那些假设字符AZ 是连续的。 99.999% 的人永远不会看到它的事实并不意味着如果出现问题,它只会影响 0.001%。
      • 但是,尽管如此,这无关紧要,因为我已经用一种更有可能的可能性代替了这种可能性。这并没有减轻早期问题的严重性。正如我所说,引导尤达:“不。尽量不要!做。或者不要!” - 要么遵循标准,要么不遵循大多数时候,你是对的,这可能无关紧要,但我更愿意安全。其他人可能有不同的优先事项。
      【解决方案4】:

      我想既然语句“count = count + 1”可能会分解成两条汇编指令,那么另一个线程有可能在这两条指令之间交换吗?不确定。你怎么看?

      不要这样想。您正在编写 C 代码和 pthreads 代码。您无需考虑汇编代码即可知道代码的行为方式。

      pthreads 标准没有定义当一个线程访问一个对象而另一个线程正在或可能正在修改它时的行为。因此,除非您正在编写特定于平台的代码,否则您应该假设该代码可以做任何事情——甚至崩溃。

      明显的 pthreads 修复是使用互斥锁。如果您的平台有原子操作,您可以使用它们。

      我强烈建议您不要深入讨论它可能会如何失败或汇编代码可能是什么样子。无论您可能认为编译器或 CPU 会做什么,也可能不认为,代码的行为是未定义的。而且很容易说服自己你已经涵盖了你能想到的所有可能失败的方法,然后你错过了一个,它失败了。

      【讨论】:

        最近更新 更多