C++ std::mutex 如何绑定到资源?
How does a C++ std::mutex bind to a resource?
编译器是否简单地检查哪些变量在锁定和解锁语句之间被修改并将它们绑定到互斥体以便对它们进行独占访问?
或者 mutex.lock()
是否锁定当前范围内可见的所有资源?
在资源使用方面,编译器根本不关心mutex.lock()
。程序员有责任确保资源访问发生在适当的范围内 locking/unlocking.
但是,编译器以一种它知道不会优化掉某些结构的方式关心互斥锁,否则它会优化 - 但您现在可能对此不感兴趣。
"mutex"是"mutual exclusion"的缩写;使用互斥量的正确原则是在输入任何修改线程间共享变量的代码之前将其锁定,并在该部分代码完成后将其解锁。如果一个线程锁定互斥量,任何其他试图锁定它的线程都将被阻塞,直到拥有该互斥量的线程将其解锁。这意味着一次只有一个线程在可以修改共享变量的代码中,并消除了竞争条件。
互斥锁的其余功能在某种程度上依赖于编译器的魔力:它还可以防止编译器将数据的加载和存储从受保护代码内部移动到外部,反之亦然,这对于受保护的代码保持受保护。
互斥量是 semaphore 的特定实现。特别是它是一个 binary 信号量。
信号量(显然在计算机科学上下文中)可以实现为整数变量和硬件或软件(操作系统)原始,它们是原子(不能被打断)。
想象一下喜欢(伪高阶代码):
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;
}
}
很明显,在纯高级语言中,这种方法行不通,因为线程可能会在测试互斥锁空闲之后和设置为已占用之前被中断。
出于这个原因,信号量和互斥锁是在原始指令的帮助下实现的,这些指令在 "one clock" 中(原子地)实现了那些 "test and set" 操作。
一个例子是 test-and-set
原语。
假设 m
是一个 std::mutex
类型的变量:
想象一下这个序列:
int a;
m.lock();
b += 1;
a = b;
m.unlock();
do_something_with(a);
这里发生了一件 'obvious' 事情:
a从b的赋值和b的自增是'protected'来自其他线程的干扰,因为其他线程会尝试锁定相同的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()
等都是fences.编译器 'knows' 它可能不会以这样一种方式重新排序加载和存储(读取和写入),即操作在您编写代码时指定的位置的另一侧结束。
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
等还可以防止内存缓存以更 'optimal' 的方式重新排序加载和存储,这在单线程环境中很好,但在多核环境中是灾难性的(即任何现代 PC或 phone) 系统。
这种安全是有代价的,因为线程 A 的缓存必须在线程 B 读取相同数据之前刷新,并且与缓存内存访问相比,将缓存刷新到内存中慢得可怕。但是这就是生活。这是确保并发执行安全的唯一方法。
这就是为什么我们更喜欢,如果可能的话,在 SMP 系统中,每个线程都有自己的数据副本来处理。我们不仅要减少花在锁上的时间,还要减少穿过栅栏的次数。
我可以继续谈论 std::memory_order
修饰符,但这是一个黑暗而危险的漏洞,专家经常犯错,初学者也无望改正。
根本就没有这样的聪明,让它正常工作是程序员的责任。
互斥体就像没有墙壁的房子上的一扇可上锁的门。
你可以用它做的就是防止其他人在锁着的时候通过门进入房子。
门只有在每个人都同意独占从门进入屋子时才有用,并且当门被锁上时等待里面的人打开门然后退出。
没有什么可以禁止坏人通过不存在的墙壁进入房屋,除非同意您不应该这样做。
编译器是否简单地检查哪些变量在锁定和解锁语句之间被修改并将它们绑定到互斥体以便对它们进行独占访问?
或者 mutex.lock()
是否锁定当前范围内可见的所有资源?
在资源使用方面,编译器根本不关心mutex.lock()
。程序员有责任确保资源访问发生在适当的范围内 locking/unlocking.
但是,编译器以一种它知道不会优化掉某些结构的方式关心互斥锁,否则它会优化 - 但您现在可能对此不感兴趣。
"mutex"是"mutual exclusion"的缩写;使用互斥量的正确原则是在输入任何修改线程间共享变量的代码之前将其锁定,并在该部分代码完成后将其解锁。如果一个线程锁定互斥量,任何其他试图锁定它的线程都将被阻塞,直到拥有该互斥量的线程将其解锁。这意味着一次只有一个线程在可以修改共享变量的代码中,并消除了竞争条件。
互斥锁的其余功能在某种程度上依赖于编译器的魔力:它还可以防止编译器将数据的加载和存储从受保护代码内部移动到外部,反之亦然,这对于受保护的代码保持受保护。
互斥量是 semaphore 的特定实现。特别是它是一个 binary 信号量。
信号量(显然在计算机科学上下文中)可以实现为整数变量和硬件或软件(操作系统)原始,它们是原子(不能被打断)。
想象一下喜欢(伪高阶代码):
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;
}
}
很明显,在纯高级语言中,这种方法行不通,因为线程可能会在测试互斥锁空闲之后和设置为已占用之前被中断。
出于这个原因,信号量和互斥锁是在原始指令的帮助下实现的,这些指令在 "one clock" 中(原子地)实现了那些 "test and set" 操作。
一个例子是 test-and-set
原语。
假设 m
是一个 std::mutex
类型的变量:
想象一下这个序列:
int a;
m.lock();
b += 1;
a = b;
m.unlock();
do_something_with(a);
这里发生了一件 'obvious' 事情:
a从b的赋值和b的自增是'protected'来自其他线程的干扰,因为其他线程会尝试锁定相同的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()
等都是fences.编译器 'knows' 它可能不会以这样一种方式重新排序加载和存储(读取和写入),即操作在您编写代码时指定的位置的另一侧结束。
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
等还可以防止内存缓存以更 'optimal' 的方式重新排序加载和存储,这在单线程环境中很好,但在多核环境中是灾难性的(即任何现代 PC或 phone) 系统。
这种安全是有代价的,因为线程 A 的缓存必须在线程 B 读取相同数据之前刷新,并且与缓存内存访问相比,将缓存刷新到内存中慢得可怕。但是这就是生活。这是确保并发执行安全的唯一方法。
这就是为什么我们更喜欢,如果可能的话,在 SMP 系统中,每个线程都有自己的数据副本来处理。我们不仅要减少花在锁上的时间,还要减少穿过栅栏的次数。
我可以继续谈论 std::memory_order
修饰符,但这是一个黑暗而危险的漏洞,专家经常犯错,初学者也无望改正。
根本就没有这样的聪明,让它正常工作是程序员的责任。
互斥体就像没有墙壁的房子上的一扇可上锁的门。
你可以用它做的就是防止其他人在锁着的时候通过门进入房子。
门只有在每个人都同意独占从门进入屋子时才有用,并且当门被锁上时等待里面的人打开门然后退出。
没有什么可以禁止坏人通过不存在的墙壁进入房屋,除非同意您不应该这样做。