互斥量是如何工作的?互斥体是否在全局范围内保护变量?它定义的范围重要吗?
How does a Mutex work? Does a mutex protect variables globally? Does the scope in which it is defined matter?
互斥锁是全局锁定对变量的访问,还是只锁定与锁定互斥锁相同范围内的变量?
请注意,我不得不更改此问题的标题,因为很多答案似乎对我的问题感到困惑。这不是关于“互斥量object”的范围(全局或其他)的问题,而是关于什么范围的变量被互斥量“锁定”的问题。
我认为答案是互斥锁锁定了对所有变量的访问,即;所有全局和局部范围的变量。 (这是互斥锁阻塞线程执行的结果,而不是访问特定内存区域的结果。)
我正在尝试了解互斥量。
我试图了解互斥体会锁定哪些内存部分,或等效地锁定哪些变量。
然而我从网上阅读的理解是互斥体做不锁定内存,它们同时锁定(或阻塞)运行 线程,它们都是同一进程的所有成员。 (对吗?)
https://mortoray.com/2011/12/16/how-does-a-mutex-work-what-does-it-cost/
所以我的问题变得简单 “互斥量是全局的吗?”
...或者它们可能是“一般来说是全球性的,但 Whosebug 社区可以想象一些它们不是的特殊情况?”
最初考虑我的问题时,我对以下示例中显示的内容感兴趣。
// 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 ???
}
}
人们可能会认为这是 pseudo-code C++ 或 C 或任何其他类似相关语言。
2021 年编辑:问题标题已更改,以更好地反映问题的内容和相关答案。
So my question has become simply "are mutexes global?"
没有。互斥锁有一个 lock() 和一个 unlock() 方法,互斥锁唯一做的就是导致它的 lock() 调用(从任何线程)不会 return 只要另一个线程锁定了该互斥锁.当锁定互斥锁的线程调用 unlock() 时,即第一个线程中的 lock() 调用将 return。这样可以保证只有一个线程持有互斥锁(即在任何给定时间在其 lock() 调用和 unlock call() 之间的区域中执行)。
仅此而已。因此互斥量只会影响在该特定互斥量上调用 lock() 的线程,而不会影响其他任何东西。
正如您的问题所暗示的,我假设您是在独立于任何编程语言提出问题。
首先了解什么是互斥量及其工作原理很重要? 互斥量是二进制信号量。那什么是信号量呢? 信号量是具有以下属性的整数,
- 您可以将其初始化为任何允许的值(对于互斥体,它是 1 或 0)。
- 线程可以访问信号量并且它可以增加或减少它的整数值。
- 当线程递减它时,
如果结果为正或零, 该线程可以继续其进程。
如果结果是否定的,该线程将等待并且信号量值将不会被任何后续线程进一步递减。
- 如果一个线程递增它,(在这种情况下,信号量值将为正或 0)并且结果为 0,则等待线程之一可以继续执行。
因此,当出现线程试图访问共享资源的情况时,它将递减互斥量值(从 0 开始,以便其他线程正在等待)。当它完成时,它将增加互斥值(以便等待线程可以继续)。这就是通过互斥量(二进制信号量)进行访问控制的方式。
我想你明白你的问题在这里不适用。作为
的简单答案
So my question has become simply "are mutexes global?"
简直就是NO.
互斥量具有您分配给它的任何范围。根据您声明它的位置和方式,它可以是全局的或本地的。例如,如果您在全局内存中的一个可以全局访问它的地方声明一个互斥锁,那么它确实是全局的。相反,如果您在函数或私有 class 范围级别声明它,则只有该函数或 class 才能访问它。
也就是说,为了对同步有用,需要在需要同步的线程可以访问的范围内声明互斥体。这是在全球范围内还是在某些本地范围内取决于您的程序结构。我建议在线程可访问的最高范围内声明它,但不要更高。
在您的特定示例中,互斥量确实是全局的,因为您已在全局内存中声明了它。
当我问这个问题的时候我很困惑...
当我最初问这个问题时,我很困惑,因为我对“互斥体”在硬件中的功能没有概念性的理解,而我对硬件中存在的许多其他事物却有概念性的理解。 (例如,编译器如何将文本转换为机器可读指令。缓存和内存如何工作。图形或协处理器如何工作。网络硬件和接口如何工作等)
误解 1:互斥锁不锁定 内存位置
当我第一次听说 Mutex 时,早在写这个问题之前,我就误解了 mutex 是一种锁定内存区域的功能。 (该区域可能是全球性的。)
事实并非如此。如果另一个线程锁定互斥量,其他线程和进程可以继续访问主内存和缓存。您可以立即看到为什么这样的设计效率低下,因为为了同步一个进程,它会阻塞所有其他系统进程。
误解2:互斥对象声明的范围无关
这里的上下文是 C 代码,C 类似语言,其中您具有由 {
和 }
定义的作用域块,但是相同的逻辑可以适用于 Python,其中作用域是由缩进定义。
我认为这种误解来自 scoped_lock
对象的存在,以及范围用于管理 Mutex 对象的生命周期(锁定和解锁、资源)的类似概念。
也有人可能会争辩说,由于可以在程序中传递指向互斥量的指针和引用,因此不能使用互斥量的范围来定义哪些变量被互斥量“锁定”。
例如,我误解了以下片段:
{
int x, y, z;
Mutex m;
m.lock();
}
我相信上面的代码片段会锁定所有其他线程对变量 x、y 和 z 的访问 因为 x、y 和 z 是在与互斥体 m 相同的范围内声明的.这也不是互斥锁的工作方式。
理解 1:互斥通常在硬件中使用原子操作实现
原子操作完全独立于互斥体的概念,但是它们是理解互斥体如何存在以及如何工作的先决条件。
当 CPU 执行类似 c = a + b
的操作时,这涉及一系列单独(原子)操作。 Atom 一词源自 Atomos,意思是“不可分割的”或“基本的”。 (原子是可分的,但是当古希腊的理论家最初设想构成物质的物体时,他们假设粒子必须可分到一些基本的最小可能成分,而这些成分本身是不可分的。他们并没有大错特错,因为原子是由其他基本粒子组成的,到目前为止我们认为这些粒子是不可分割的。)
言归正传:c = a + b
是这样的:
- 从内存中加载 a 到寄存器 1
- 从内存中加载 b 到寄存器 2
- 做add操作:将寄存器2的内容加到寄存器1中,结果在寄存器1中
- 将寄存器1保存到内存c
添加操作可能需要几个时钟周期,而 loading/saving 到内存在现代 x86 机器上通常需要 100 个时钟周期。然而,每个操作都是原子的,因为单个 CPU 指令正在完成,并且该指令不能分成任何更小的更小指令步骤。这些指令本身就是基本的计算操作。
理解了这一点,就存在一组原子指令,可以执行以下操作:
- 从内存中加载一个值,将其递增并保存到内存中
- 从内存中加载一个值,将其递减并保存到内存中
- 从内存中加载一个值,将其与已经加载到寄存器中的值进行比较,并根据比较结果进行分支
请注意,此类操作通常比对应的非原子序列操作慢得多。这是因为在执行上述指令时,流水线等优化是无效的。 (我觉得?)
在这一点上,我的知识变得有点不准确,而且更加手摇,但据我所知,这些操作通常是通过处理器内部的一些数字逻辑来实现的,它阻止了所有其他进程 运行 当这些原子操作(上面列出的)正在执行时。
含义:如果有8个CPU个核心运行,如果一个核心遇到像上面这样的指令,它会通知其他核心停止运行直到它完成那个原子操作。 (这至少是近似于这些线的东西。)
理解二:实际的互斥操作
鉴于上述情况,可以使用这些原子机器指令实现互斥锁。此处发布的其他答案提出了可能的方法,包括类似于引用计数的方法。 (信号量。)
C++ 中的实际互斥量是这样工作的:
- 每个互斥锁对象在内存中都有一个变量与之关联,这个变量的值表示一个互斥锁是否被锁定
- 使用 CPU 支持的特殊原子操作更新此互斥锁变量,目的是允许对互斥锁进行编程
- 内存中的其他地方还有一些其他 variables/data 您想要 protect/synchronize 访问
- 此同步是使用互斥锁完成的 variable/data
- 在线程 reads/writes 到某些 data/variable 之前,需要由所有操作它的线程互斥访问,该线程必须首先“锁定”特殊互斥体 data/variable
- 这是使用 CPU 中内置的原子操作完成的,目的是支持互斥编程
所以你看,被“锁定”和互斥访问的数据完全独立于用于存储互斥锁状态的实际数据。
- 如果另一个线程想要read/write必须互斥访问的数据,它将尝试锁定互斥量。如果互斥量已经被锁定,这意味着另一个线程有权访问该数据,并且不允许其他线程访问,因此该线程通常会进入休眠状态,并在互斥量被锁定时被操作系统重新唤醒接下来解锁。
重要的是要注意操作系统线程(内核)与互斥过程密切相关。通常,在线程休眠之前,它会告诉操作系统它希望在互斥量空闲时再次被唤醒。当其他线程锁定或解锁互斥锁时,操作系统也会收到通知。因此,有关互斥锁状态的信息同步是通过操作系统内核的消息传递的。
这就是为什么编写多线程 OS 内核(可能)是不可能的(如果不是很困难的话)。我不知道这是否真的成功完成了。这听起来像是一个难题,可能是当前CS研究的主题。
这几乎是我对这个主题的了解。显然我的知识不是绝对的...
注意:请随时在评论部分更正我的希腊语历史或 x86 机器指令知识。毫无疑问,并非这里的所有内容都是完全准确的。
互斥锁是全局锁定对变量的访问,还是只锁定与锁定互斥锁相同范围内的变量?
请注意,我不得不更改此问题的标题,因为很多答案似乎对我的问题感到困惑。这不是关于“互斥量object”的范围(全局或其他)的问题,而是关于什么范围的变量被互斥量“锁定”的问题。
我认为答案是互斥锁锁定了对所有变量的访问,即;所有全局和局部范围的变量。 (这是互斥锁阻塞线程执行的结果,而不是访问特定内存区域的结果。)
我正在尝试了解互斥量。
我试图了解互斥体会锁定哪些内存部分,或等效地锁定哪些变量。
然而我从网上阅读的理解是互斥体做不锁定内存,它们同时锁定(或阻塞)运行 线程,它们都是同一进程的所有成员。 (对吗?)
https://mortoray.com/2011/12/16/how-does-a-mutex-work-what-does-it-cost/
所以我的问题变得简单 “互斥量是全局的吗?”
...或者它们可能是“一般来说是全球性的,但 Whosebug 社区可以想象一些它们不是的特殊情况?”
最初考虑我的问题时,我对以下示例中显示的内容感兴趣。
// 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 ???
}
}
人们可能会认为这是 pseudo-code C++ 或 C 或任何其他类似相关语言。
2021 年编辑:问题标题已更改,以更好地反映问题的内容和相关答案。
So my question has become simply "are mutexes global?"
没有。互斥锁有一个 lock() 和一个 unlock() 方法,互斥锁唯一做的就是导致它的 lock() 调用(从任何线程)不会 return 只要另一个线程锁定了该互斥锁.当锁定互斥锁的线程调用 unlock() 时,即第一个线程中的 lock() 调用将 return。这样可以保证只有一个线程持有互斥锁(即在任何给定时间在其 lock() 调用和 unlock call() 之间的区域中执行)。
仅此而已。因此互斥量只会影响在该特定互斥量上调用 lock() 的线程,而不会影响其他任何东西。
正如您的问题所暗示的,我假设您是在独立于任何编程语言提出问题。
首先了解什么是互斥量及其工作原理很重要? 互斥量是二进制信号量。那什么是信号量呢? 信号量是具有以下属性的整数,
- 您可以将其初始化为任何允许的值(对于互斥体,它是 1 或 0)。
- 线程可以访问信号量并且它可以增加或减少它的整数值。
- 当线程递减它时, 如果结果为正或零, 该线程可以继续其进程。 如果结果是否定的,该线程将等待并且信号量值将不会被任何后续线程进一步递减。
- 如果一个线程递增它,(在这种情况下,信号量值将为正或 0)并且结果为 0,则等待线程之一可以继续执行。
因此,当出现线程试图访问共享资源的情况时,它将递减互斥量值(从 0 开始,以便其他线程正在等待)。当它完成时,它将增加互斥值(以便等待线程可以继续)。这就是通过互斥量(二进制信号量)进行访问控制的方式。
我想你明白你的问题在这里不适用。作为
的简单答案So my question has become simply "are mutexes global?"
简直就是NO.
互斥量具有您分配给它的任何范围。根据您声明它的位置和方式,它可以是全局的或本地的。例如,如果您在全局内存中的一个可以全局访问它的地方声明一个互斥锁,那么它确实是全局的。相反,如果您在函数或私有 class 范围级别声明它,则只有该函数或 class 才能访问它。
也就是说,为了对同步有用,需要在需要同步的线程可以访问的范围内声明互斥体。这是在全球范围内还是在某些本地范围内取决于您的程序结构。我建议在线程可访问的最高范围内声明它,但不要更高。
在您的特定示例中,互斥量确实是全局的,因为您已在全局内存中声明了它。
当我问这个问题的时候我很困惑...
当我最初问这个问题时,我很困惑,因为我对“互斥体”在硬件中的功能没有概念性的理解,而我对硬件中存在的许多其他事物却有概念性的理解。 (例如,编译器如何将文本转换为机器可读指令。缓存和内存如何工作。图形或协处理器如何工作。网络硬件和接口如何工作等)
误解 1:互斥锁不锁定 内存位置
当我第一次听说 Mutex 时,早在写这个问题之前,我就误解了 mutex 是一种锁定内存区域的功能。 (该区域可能是全球性的。)
事实并非如此。如果另一个线程锁定互斥量,其他线程和进程可以继续访问主内存和缓存。您可以立即看到为什么这样的设计效率低下,因为为了同步一个进程,它会阻塞所有其他系统进程。
误解2:互斥对象声明的范围无关
这里的上下文是 C 代码,C 类似语言,其中您具有由 {
和 }
定义的作用域块,但是相同的逻辑可以适用于 Python,其中作用域是由缩进定义。
我认为这种误解来自 scoped_lock
对象的存在,以及范围用于管理 Mutex 对象的生命周期(锁定和解锁、资源)的类似概念。
也有人可能会争辩说,由于可以在程序中传递指向互斥量的指针和引用,因此不能使用互斥量的范围来定义哪些变量被互斥量“锁定”。
例如,我误解了以下片段:
{
int x, y, z;
Mutex m;
m.lock();
}
我相信上面的代码片段会锁定所有其他线程对变量 x、y 和 z 的访问 因为 x、y 和 z 是在与互斥体 m 相同的范围内声明的.这也不是互斥锁的工作方式。
理解 1:互斥通常在硬件中使用原子操作实现
原子操作完全独立于互斥体的概念,但是它们是理解互斥体如何存在以及如何工作的先决条件。
当 CPU 执行类似 c = a + b
的操作时,这涉及一系列单独(原子)操作。 Atom 一词源自 Atomos,意思是“不可分割的”或“基本的”。 (原子是可分的,但是当古希腊的理论家最初设想构成物质的物体时,他们假设粒子必须可分到一些基本的最小可能成分,而这些成分本身是不可分的。他们并没有大错特错,因为原子是由其他基本粒子组成的,到目前为止我们认为这些粒子是不可分割的。)
言归正传:c = a + b
是这样的:
- 从内存中加载 a 到寄存器 1
- 从内存中加载 b 到寄存器 2
- 做add操作:将寄存器2的内容加到寄存器1中,结果在寄存器1中
- 将寄存器1保存到内存c
添加操作可能需要几个时钟周期,而 loading/saving 到内存在现代 x86 机器上通常需要 100 个时钟周期。然而,每个操作都是原子的,因为单个 CPU 指令正在完成,并且该指令不能分成任何更小的更小指令步骤。这些指令本身就是基本的计算操作。
理解了这一点,就存在一组原子指令,可以执行以下操作:
- 从内存中加载一个值,将其递增并保存到内存中
- 从内存中加载一个值,将其递减并保存到内存中
- 从内存中加载一个值,将其与已经加载到寄存器中的值进行比较,并根据比较结果进行分支
请注意,此类操作通常比对应的非原子序列操作慢得多。这是因为在执行上述指令时,流水线等优化是无效的。 (我觉得?)
在这一点上,我的知识变得有点不准确,而且更加手摇,但据我所知,这些操作通常是通过处理器内部的一些数字逻辑来实现的,它阻止了所有其他进程 运行 当这些原子操作(上面列出的)正在执行时。
含义:如果有8个CPU个核心运行,如果一个核心遇到像上面这样的指令,它会通知其他核心停止运行直到它完成那个原子操作。 (这至少是近似于这些线的东西。)
理解二:实际的互斥操作
鉴于上述情况,可以使用这些原子机器指令实现互斥锁。此处发布的其他答案提出了可能的方法,包括类似于引用计数的方法。 (信号量。)
C++ 中的实际互斥量是这样工作的:
- 每个互斥锁对象在内存中都有一个变量与之关联,这个变量的值表示一个互斥锁是否被锁定
- 使用 CPU 支持的特殊原子操作更新此互斥锁变量,目的是允许对互斥锁进行编程
- 内存中的其他地方还有一些其他 variables/data 您想要 protect/synchronize 访问
- 此同步是使用互斥锁完成的 variable/data
- 在线程 reads/writes 到某些 data/variable 之前,需要由所有操作它的线程互斥访问,该线程必须首先“锁定”特殊互斥体 data/variable
- 这是使用 CPU 中内置的原子操作完成的,目的是支持互斥编程
所以你看,被“锁定”和互斥访问的数据完全独立于用于存储互斥锁状态的实际数据。
- 如果另一个线程想要read/write必须互斥访问的数据,它将尝试锁定互斥量。如果互斥量已经被锁定,这意味着另一个线程有权访问该数据,并且不允许其他线程访问,因此该线程通常会进入休眠状态,并在互斥量被锁定时被操作系统重新唤醒接下来解锁。
重要的是要注意操作系统线程(内核)与互斥过程密切相关。通常,在线程休眠之前,它会告诉操作系统它希望在互斥量空闲时再次被唤醒。当其他线程锁定或解锁互斥锁时,操作系统也会收到通知。因此,有关互斥锁状态的信息同步是通过操作系统内核的消息传递的。
这就是为什么编写多线程 OS 内核(可能)是不可能的(如果不是很困难的话)。我不知道这是否真的成功完成了。这听起来像是一个难题,可能是当前CS研究的主题。
这几乎是我对这个主题的了解。显然我的知识不是绝对的...
注意:请随时在评论部分更正我的希腊语历史或 x86 机器指令知识。毫无疑问,并非这里的所有内容都是完全准确的。