编译器是否只是检查lock和unlock语句之间正在修改哪些变量并将它们绑定到互斥锁,以便对它们进行独占访问?
或者mutex.lock()
是否锁定了当前范围内可见的所有资源?
答案 0 :(得分:16)
鉴于m
是std::mutex
类型的变量:
想象一下这个序列:
int a;
m.lock();
b += 1;
a = b;
m.unlock();
do_something_with(a);
有一个明显的'发生在这里的事情:
a的分配和b的增量受到保护'来自其他线程的干扰,因为其他线程将尝试锁定相同的m
,并且在我们调用m.unlock()
之前将被阻止。
还有一个更微妙的事情发生。
在单线程代码中,编译器将寻求重新加载加载和存储。如果没有锁定,编译器可以自由地重新编写代码,如果这对你的芯片组更有效:
int a = b + 1;
// m.lock();
b = a;
// m.unlock();
do_something_with(a);
甚至:
do_something_with(++b);
但是,std::mutex::lock()
,unlock()
,std::thread()
,std::async()
,std::future::get()
等围栏。编译器知道'它可能不会重新排序加载和存储(读取和写入),使得操作最终在您编写代码时指定的栅栏的另一侧。
1:
2: m.lock(); <--- This is a fence
3: b += 1; <--- So this load/store operation may not move above line 2
4: m.unlock(); <-- Nor may it be moved below this line
想象一下,如果不是这样的话会发生什么:
(重新排序的代码)
thread1: int a = b + 1;
<--- Here another thread precedes us and executes the same block of code
thread2: int a = b + 1;
thread2: m.lock();
thread2: b = a;
thread2: m.unlock();
thread1: m.lock();
thread1: b = a;
thread1: m.unlock();
thread1:do_something_with(a);
thread2:do_something_with(a);
如果你遵循它,你会发现b现在有错误的值,因为编译器正在努力使你的代码更快。
......而且那只是编译器优化。 std::mutex
等也可以防止内存缓存重新排序加载和存储更加优化的内容。这种方式在单线程环境中会很好,但在多核(即任何现代PC或电话)系统中都是灾难性的。
这种安全性需要付出代价,因为在线程B读取相同数据之前必须刷新线程A的高速缓存,并且与高速缓存的内存访问相比,将高速缓存刷新到内存的速度非常慢。但c&#39; est la vie。这是使并发执行安全的唯一方法。
这就是为什么我们希望在SMP系统中,如果可能的话,每个线程都有自己的数据副本。我们希望不仅最大限度地减少锁定所花费的时间,还要最小化我们越过栅栏的次数。
我可以继续讨论std::memory_order
修饰符,但这是一个黑暗而危险的漏洞,专家经常会出错并且初学者没有希望能够做到这一点。
答案 1 :(得分:6)
“互斥”是“互斥”的缩写;使用互斥锁的正确规则是在输入修改线程之间共享的变量的任何代码之前锁定它,并在完成代码部分时将其解锁。如果一个线程锁定互斥锁,则任何其他尝试锁定互斥锁的线程都将被阻塞,直到拥有该互斥锁的线程将其解锁。这意味着一次只有一个线程在代码中,可以修改共享变量,从而消除了竞争条件。
互斥体的其余部分在某种程度上依赖于编译器魔法:它还阻止编译器将受保护代码内部的数据加载和存储移动到其外部,反之亦然,这对于受保护的代码是必需的保持受到保护。
答案 2 :(得分:3)
互斥是semaphore的特定实现。特别是它是二进制信号量。
信号量(显然在计算机科学上下文中)可以实现为整数变量和硬件或软件(操作系统)原语 atomic (不能被中断) )。
想象一下喜欢(伪高级代码):
int mutex = 1; // The mutex is free when created (free=1, occupied=0).
// in a concurrency block
{
:try-again
// test if mutex is 1 (is free).
if (mutex > 0) {
// mutex WAS free so now I set as occupied (set 0)
--mutex;
// Now I've 'acquired' the mutex and since the mutex is 0 or 1
// only me can be here.
// Do something in mutual exclusion
// Done.
// Unlock the mutex
++mutex;
// Now the mutex is free so other threads can acquire it.
} else {
// Mutex is 0 so I tried but it's already occupied.
// Block the thread and try again.
goto try-again;
}
}
在纯粹的高级语言中很明显,这种方法无法工作,因为线程在测试互斥锁是免费的之后可以被中断,然后才能设置为占用。
由于这个原因,信号量和互斥量是在原始指令的帮助下实现的,这些指令在“一个时钟”(原子)中实现那些“测试和设置”操作。
一个例子是test-and-set
原语。
答案 3 :(得分:3)
根本没有这样的聪明,让它正常工作是程序员的责任。
互斥锁就像一扇没有墙壁的房子上的可上锁的门。
当你被锁定时,你可以做的就是阻止他人通过门进入房屋。 只有当每个人都同意通过门进入房屋时,门才有用,当门被锁上时,等待内部人员解锁门并退出。 除了你不应该达成的协议之外,没有什么能阻止一个坏人通过不存在的墙进入房子。
答案 4 :(得分:1)
在资源使用方面,编译器对mutex.lock()
的关注度不高。程序员有责任确保在正确的锁定/解锁过程中进行资源访问。
然而,编译器关心的是一个互斥体,它知道不会优化某些构造,否则你可能会对此感兴趣。但是你现在可能对此不感兴趣。