【问题标题】:Thread mutex behaviour线程互斥行为
【发布时间】:2011-01-17 22:48:06
【问题描述】:

我正在学习 C。我正在编写一个具有多个线程的应用程序;我知道当一个变量在两个或多个线程之间共享时,最好使用互斥锁来锁定/解锁,以避免死锁和变量不一致。当我想更改或查看一个变量时,这一点非常清楚。

int i = 0; /** Global */
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

/** Thread 1. */
pthread_mutex_lock(&mutex);
i++;
pthread_mutex_unlock(&mutex);

/** Thread 2. */
pthread_mutex_lock(&mutex);
i++;
pthread_mutex_unlock(&mutex);

我认为这是正确的。变量i,在执行结束时,包含整数2
无论如何,在某些情况下,我不知道将两个函数调用放在哪里。

例如,假设您有一个函数obtain(),它返回一个全局变量。我需要从两个线程中调用该函数。我还有另外两个线程调用函数set(),定义了一些参数;此函数将设置相同的全局变量。当您需要在获取/设置 var 之前执行某些操作时,这两个函数是必需的。

/** (0) */
/** Thread 1, or 2, or 3... */
if(obtain() == something) {

    if(obtain() == somethingElse) {
        // Do this, sometimes obtain() and sometimes set(random number) (1)   
    } else {
        // Do that, just obtain(). (2)
    }

} else {
    // Do this and do that (3)
    // If # of thread * 3 > 10, then set(3*10) For example. (4)
}
/** (5) */

我必须在哪里锁定,我必须在哪里解锁? 我认为情况可能更加复杂。我将不胜感激。

提前谢谢你。
——阿尔贝托

【问题讨论】:

    标签: c mutex


    【解决方案1】:

    没有任何保护:

    操作系统可以随时中断您的每个线程,并将处理器交给另一个。 “任何时候”包括“实际上来自同一 C 命令的两个汇编指令之间”。

    现在,假设您的变量在 32 位处理器中占用 64 位。这意味着您的变量占用了两个处理器“字”。为了编写它,处理器需要两条汇编指令。读书也一样。如果线程在两者之间被中断,你就会遇到麻烦。

    为了给出一个更清楚的例子,我将使用两个十进制数字的类比来表示两个二进制 32 位字。假设您在一位处理器中增加两位十进制数。要将 19 增加到 20,您必须读取 19,进行数学运算,然后写入 20。要写入 20,您必须先写入 2,然后写入 0(反之亦然)。如果你写 2,然后在写 0 之前被打断,内存中的数字将是 29,与实际正确的数字相去甚远。然后另一个线程继续读取错误的数字。

    即使您只有一个数字,仍然存在读取-修改-写入问题 Blank Xavier 解释。

    使用互斥锁

    当线程 A 锁定互斥体时,线程 A 会检查互斥体变量。如果它是空闲的,则线程 A 将其写入已占用。它使用一条原子指令、一条汇编指令来完成它,因此没有“中间”可以中断。然后它继续将 19 增加到 20。它仍然可以在不正确的 29 变量值期间被中断,但没关系,因为现在没有其他人可以访问该变量。当线程 B 试图锁定互斥锁时,它会检查互斥锁变量,它被占用。所以线程 B 知道它不能触及变量。然后它调用操作系统,说“我现在放弃处理器”。如果线程 B 再次获得处理器,它将重复该操作。然后再次。直到线程 A 最终取回处理器,完成它正在做的事情,然后解锁互斥体。

    那么,什么时候锁定?

    因为很多事情,这取决于。主要取决于您的应用程序需要正常工作的特定行为顺序。您需要在阅读之前始终锁定写作以获得保护,然后再解锁。但是“锁定的代码块”可能有很多命令,也可能只有一个。记住上面解释的舞蹈,并考虑你的应用程序应该如何表现。

    还有性能问题。如果您锁定/解锁每一行代码,您就会浪费时间锁定/解锁。如果您只在大块代码周围锁定/解锁,那么每个线程将等待很长时间让另一个线程释放互斥锁。

    并不是真的“总是”

    现在,在某些情况下您可以跳过锁定-解锁。当您处理一位数(意味着一个处理器字)变量时,它们会发生,并且每个线程要么只读取它,要么只写入它,因此读取的值不会确定稍后写入它的值。仅当您非常确定自己在做什么并且确实需要提高性能时才这样做。

    【讨论】:

    • 写出如此完整且具有启发性的答案的人应该有超过 466 次代表。 +1!
    • 谢谢。这个答案对我帮助很大。
    【解决方案2】:

    一些解释。

    在示例代码中,单个变量正在递增。

    现在,内存和 CPU 缓存的组织方式使得无论何时访问内存,都会一次访问一个缓存行中的数据。这是因为内存开始访问的速度很慢,但继续访问的速度相对较快,而且通常情况下,当访问一个位时,会访问相当数量的后续位。

    所以,我们读入我们的整数。假设整数长度为 8 个字节,缓存行也是 8 个字节(例如现代 64 位 Intel CPU)。在这种情况下,读取是必要的,因为我们需要知道原始值。因此,读取发生并且缓存行进入 L3、L2 和 L1 缓存(Intel 使用包含式缓存;L1 中的所有内容都存在于 L2 中,L2 中的所有内容都存在于 L3 中,等等)。

    现在,当您有多个 CPU 时,它们会密切关注其他 CPU 正在做什么,因为如果另一个 CPU 写入您在缓存中的缓存行,您的副本将不再正确。

    如果我们有一个 CPU 有这个缓存行,它在缓存中并且它增加值,任何其他有这个值副本的 CPU 都将其副本标记为无效。

    假设我们有两个线程,在不同的 CPU 上。他们都读入整数。此时,他们的缓存将这个缓存行标记为共享。然后其中一个写信给它。写入者将他的缓存行标记为已修改,第二个 CPU 的缓存行无效 - 所以当他尝试写入时,然后发生的事情是他再次尝试从内存中读取整数,但由于存在另一个CPU 缓存修改后的副本,他从第一个 CPU 获取修改值的副本,第一个 CPU 将其副本标记为无效,现在第二个 CPU 写入自己的新值。

    所以,到目前为止一切似乎都很好 - 我们怎么可能需要锁定?

    问题是这样的;一个 CPU 读取其缓存中的值 - 然后另一个 CPU 执行相同操作。缓存行当前标记为共享,所以这很好。然后它们都增加。其中一个将回写,因此他的高速缓存行变为独占,而所有其他 CPU 的高速缓存行被标记为无效。然后第二个 CPU 回写,这会导致它从当前所有者那里获取缓存行的副本,然后对其进行修改 - 写回相同的值。

    因此,其中一个增量丢失了。

    【讨论】:

      【解决方案3】:

      如果你有一个obtain() 函数,那么应该有一个release() 函数,对吧?然后在 get() 中加锁,在 release() 中解锁。

      【讨论】:

        【解决方案4】:

        您必须对应该是原子的整个操作持有一个锁 - 即应该作为一个不可分割的操作执行的块。

        【讨论】:

          猜你喜欢
          • 2013-01-31
          • 2014-05-02
          • 2016-12-27
          • 1970-01-01
          • 2023-02-06
          • 1970-01-01
          • 2011-03-09
          • 2012-07-09
          • 1970-01-01
          相关资源
          最近更新 更多