互斥体如何工作?互斥体是否可以全局保护变量?它的定义范围重要吗?

use*_*501 6 parallel-processing multithreading mutex atomic

互斥锁是否全局锁定对变量的访问,或者仅锁定与锁定互斥锁相同范围内的变量的访问?

请注意,我必须更改这个问题的标题,因为很多答案似乎对我的问题感到困惑。这不是关于“互斥体对象”的范围(全局或其他)的问题,而是关于互斥体“锁定”变量的范围的问题。

我相信答案是互斥体锁定对所有变量的访问,即;所有全局和局部范围的变量。(这是互斥体阻塞线程执行而不是访问特定内存区域的结果。)

我正在尝试理解互斥体。

我试图了解互斥体将锁定内存的哪些部分,或者等效地,哪些变量。

然而,我从网上阅读的理解是,互斥体不会锁定内存,它们会锁定(或阻止)同时运行的线程,这些线程都是同一进程的成员。(那是对的吗?)

https://mortoray.com/2011/12/16/how-does-a-mutex-work-what-does-it-cost/

所以我的问题就变成了“互斥体是全局的吗?”

...或者它们可能“一般来说是全球性的,但 stackoverflow 社区可以想象一些特殊情况,但它们不是?”

最初考虑我的问题时,我对以下示例所示的内容感兴趣。

// both in global scope, this mutex will lock any global scope variable?
int global_variable;
mutex global_variable_mutex;

int main()
{
    // one thread operates here and locks global_variable_mutex
    // before reading/writing

    {
        // local variables in a loop
        // launch some threads here, and wait later
        int local_variable;
        mutex local_variable_mutex;
        // wait for launched thread to return

        // does the mutex here prevent data races to the variable
        // global_variable ???
    }
}
Run Code Online (Sandbox Code Playgroud)

人们可能会假设这是 C++ 或 C 或任何其他类似相关语言的伪代码。

2021 年编辑:问题标题已更改,以更好地反映问题的内容和相关答案。

Jer*_*ner 7

所以我的问题就变成了“互斥体是全局的吗?”

不。互斥体有一个方法lock()和一个unlock()方法,互斥体所做的唯一事情就是lock()只要另一个线程锁定了该互斥体,就导致其调用(来自任何线程)不会返回。当持有互斥锁的线程调用 时unlock(),即该lock()调用将在第一个线程中返回。这样可以保证在任何给定时间只有一个线程持有互斥锁(即在其lock()调用和unlock()调用之间的区域中执行)。

这就是全部内容了。因此,互斥锁只会影响调用该特定互斥锁的线程lock(),而不会影响其他线程。


互斥体代表“互斥”——正确使用互斥体可确保一次只有一个线程执行受同一互斥体保护的任何“关键部分”。

如果您仅在受同一互斥体保护的关键部分内修改某些变量,则您的代码不会出现数据争用。无论它们是全局的、静态的,还是由不同线程中的不同变量指向,或者两个线程可能以任何其他方式引用同一对象。

  • 因此,为了进一步澄清,互斥体实现中没有任何内容说明哪些数据受哪个互斥体保护。这都是按照惯例。如果程序员决定某个变量受某个互斥体保护,则程序员有责任确保该互斥体在访问该变量之前始终处于锁定状态。如果程序员忘记这样做,编译器将不会吐出任何错误消息。该程序在大多数情况下都运行良好。 (2认同)

use*_*501 5

当我问这个问题的时候我很困惑......

当我最初问这个问题时,我很困惑,因为我对“互斥体”在硬件中如何工作没有概念性的理解,而我确实对硬件中存在的许多其他事物有概念性的理解。(例如,编译器如何将文本转换为机器可读指令。缓存和内存如何工作。图形或协处理器如何工作。网络硬件和接口如何工作等)

误解 1:互斥锁不锁定内存位置

当我第一次听说互斥体时,早在写这个问题之前,我就将互斥体误解为锁定内存区域的功能。(该地区可能是全球性的。)

事实并非如此。如果另一个线程锁定互斥锁,其他线程和进程可以继续访问主内存和缓存。您立即就会明白为什么这样的设计效率低下,因为它会为了同步一个系统进程而阻塞所有其他系统进程。

误解2:互斥对象的声明范围无关紧要

其上下文是 C 代码和类似 C 的语言,其中您具有由 定义的作用域块{}但是相同的逻辑也适用于 Python,其中作用域由缩进定义。

我认为这种误解来自scoped_lock对象的存在,以及类似的概念,其中范围用于管理互斥对象的生命周期(锁定和解锁、资源)。

人们还可能会争辩说,由于对互斥锁的指针和引用可以在程序中传递,因此互斥锁的范围不能用于定义互斥锁“锁定”的变量。

例如,我误解了以下代码片段:

{
    int x, y, z;
    Mutex m;
    m.lock();
}
Run Code Online (Sandbox Code Playgroud)

我相信上面的代码片段会锁定所有其他线程对变量 x、y 和 z 的访问,因为 x、y 和 z 是在与互斥体 m 相同的范围内声明的。这也不是互斥锁的工作原理。

理解1:互斥体通常使用原子操作在硬件中实现

原子操作与互斥体的概念完全分开,但它们是理解互斥体如何存在以及如何工作的先决条件。

当 CPU 执行类似的操作时c = a + b,这涉及一系列单独的(原子)操作。Atom 这个词源自 Atomos,意思是“不可分割的”或“基本的”。(原子是可分的,但是当古希腊的理论家最初构想构成物质的物体时,他们假设粒子必须可分到一些基本的最小可能成分,而该成分本身是不可分的。他们并没有错得太离谱,因为原子是由其他基本粒子组成的,到目前为止我们认为这些基本粒子是不可分割的。)

回到正题:c = a + b大致如下:

  • 将 a 从内存加载到寄存器 1
  • 将 b 从内存加载到寄存器 2
  • 做加法操作:将寄存器2的内容与寄存器1相加,结果在寄存器1中
  • 将寄存器 1 保存到内存 c

添加操作可能需要几个时钟周期,而在现代 x86 机器上加载/保存到内存通常需要 100 个时钟周期。然而,每个操作都是原子的,因为单个 CPU 指令正在完成,并且该指令不能分为更小的指令的任何更小的步骤。这些指令本身就是基本的计算操作。

了解了这一点,就存在一组原子指令,可以执行以下操作:

  • 从内存中加载一个值,将其递增并将其保存到内存中
  • 从内存中加载一个值,将其递减并将其保存到内存中
  • 从内存中加载一个值,将其与已加载到寄存器中的值进行比较,然后根据比较结果进行分支

请注意,此类操作通常比非原子序列操作慢得多。这是因为执行上述指令时需要进行流水线等优化。(我认为?)

在这一点上,我的知识变得不太准确,而且更加手动,但据我了解,这些操作通常是通过处理器内部的一些数字逻辑来实现的,这些逻辑在这些原子操作(上面列出的)时阻止所有其他进程运行)正在执行。

含义:如果有 8 个 CPU 核心在运行,如果其中一个核心遇到类似上述的指令,它会通知其他核心停止运行,直到它完成该原子操作。(至少大致是这样的。)

理解2:实际的互斥操作

鉴于上述情况,可以使用这些原子机器指令来实现互斥锁。此处发布的其他答案提出了可能的方法,包括类似于引用计数的方法。(信号。)

C++ 中的实际互斥体的工作原理是这样的:

  • 每个互斥体对象在内存中都有一个与其关联的变量,该变量的值指示互斥体是否被锁定
  • 此互斥体变量使用 CPU 支持的特殊原子操作进行更新,以便允许对互斥体进行编程
  • 在内存的其他地方,还有一些您想要保护/同步访问的其他变量/数据
  • 此同步是使用互斥变量/数据完成的
  • 在线程读取/写入某些需要由对其进行操作的所有线程互斥访问的某些数据/变量之前,该线程必须首先“锁定”特殊的互斥数据/变量
  • 这是使用 CPU 内置的原子操作来完成的,目的是支持互斥编程

所以你看,被“锁定”和互斥访问的数据完全独立于用于存储互斥体状态的实际数据。

  • 如果另一个线程想要读/写必须互斥访问的数据,它将尝试锁定互斥体。如果互斥量已经被锁定,则意味着另一个线程有权访问该数据,并且不允许其他线程访问该数据,因此该线程通常会进入睡眠状态,并且当互斥量被锁定时会被操作系统重新唤醒。接下来解锁。

值得注意的是,操作系统线程(内核)在互斥进程中至关重要。通常,在线程休眠之前,它会告诉操作系统它希望在互斥体空闲时再次被唤醒。当其他线程锁定或解锁互斥锁时,操作系统也会收到通知。因此,有关互斥体状态的信息同步是通过操作系统内核的消息传递的。

这就是为什么编写多线程操作系统内核(可能)是不可能的(如果不是非常困难的话)。我不知道这是否真的成功了。这听起来像是一个难题,可能是当前计算机科学研究的主题。

这几乎是我所知道的关于这个主题的一切。显然我的知识不是绝对的......

注意:请随时在评论部分纠正我的希腊历史或 x86 机器指令知识。毫无疑问,并非所有内容都完全准确。