与互斥锁和宽松的内存顺序原子同步
Synchronising with mutex and relaxed memory order atomic
我有一个共享数据结构,它已经在内部与互斥体同步。我可以使用一个内存顺序放松的原子来表示变化吗?我在代码中的意思的一个非常简化的视图
线程 1
shared_conf.set("somekey","someval");
is_reconfigured.store(true, std::memory_order_relaxed);
线程 2
if (is_reconfigured.load(std::memory_order_relaxed)) {
inspect_shared_conf();
}
我能保证在 shared_map 中看到更新吗?;共享映射本身在内部将每个 write/read 调用与互斥锁
同步
宽松的顺序意味着原子和外部操作的顺序只发生在特定原子对象上的操作(即使这样,编译器也可以在 program-defined 之外自由 re-order 它们命令)。因此,轻松存储与外部对象中的任何状态没有关系。所以宽松的负载不会与您的其他互斥锁同步。
acquire/release 语义的全部要点是允许原子控制其他内存的可见性。如果你想让一个原子加载意味着某物可用,它必须是一个获取并且它获取的值必须已经被释放。
您的示例代码可以运行,是的,您将看到更新。轻松的排序将为您提供正确的行为。也就是说,就性能而言,它实际上可能不是最佳的。
让我们看一个更具体的例子,其中互斥锁是显式的。
std::mutex m;
std::atomic<bool> updated;
foo shared;
void thr1() {
m.lock();
shared.write(new_data);
m.unlock();
updated.store(true, std::memory_order_relaxed);
}
void thr2() {
if (updated.load(std::memory_order_relaxed)) {
m.lock();
data = shared.read();
m.unlock();
}
}
通俗解释
m.lock()
是获取操作,m.unlock()
是释放操作。这意味着没有什么可以阻止 updated.store(true)
向上“浮动”到临界区,超过 m.unlock()
甚至 shared.write()
。乍一看这似乎很糟糕,因为 updated
标志的全部意义在于表示 shared.write()
已经完成。但是在那种情况下并没有实际的伤害发生,因为 thr1 仍然持有互斥量 m
,因此如果 thr2 开始尝试读取共享数据,它只会等到 thr1 删除它。
真正糟糕的是,如果 updated.store()
一直飘过 m.lock()
;然后 thr2 可能会看到 updated.load() == true
并在 thr1 之前获取互斥量。但是,由于获取语义 m.lock()
.
,这不会发生
thr2
中可能存在一些相关问题(稍微复杂一些,因为它们必须是推测性的)但同样的事实再次拯救了我们:updated.load()
可以向下沉入关键部分,但没有完全超过它(因为 m.unlock()
是发布)。
但这是一个实例,其中 updated
操作的更强内存顺序虽然看起来更昂贵,但实际上可能会提高性能。如果 updated
中的值 true
过早可见,则 thr2 会在它已被 thr1 锁定时尝试锁定 m
,因此 thr2 在等待 [=19] 时将不得不阻塞=] 可用。但是如果改成updated.store(true, std::memory_order_release)
和updated.load(std::memory_order_acquire)
,那么updated
中的值true
只有在m
真正被thr1解锁后才能可见,所以m.lock()
在 thr2 中应该总是立即成功(忽略可能存在的任何其他线程的争用)。
证明
好的,这是一个非正式的解释,但我们知道在考虑内存排序时这些总是有风险的。让我们从C++内存模型的形式规则上给出一个证明。我将遵循 C++20 标准,因为我手头有它,但我认为与 C++17 相比没有任何重大的相关变化。有关此处使用的术语的定义,请参阅 [intro.races]。
我声称,如果 shared.read()
完全执行,那么 shared.write(new_data)
会在 之前发生,因此 write-read 一致性 [intro.races p18] shared.read()
会看到新的数据。
m
上的锁定和解锁操作完全有序 [thread.mutex.requirements.mutex p5]。考虑两种情况:thr1 的解锁先于 thr2 的锁定(情况 I),反之亦然(情况 II)。
案例一
如果在 m
的锁定顺序中 thr1 的解锁在 thr2 的锁定之前,则没有问题;它们 彼此同步 [thread.mutex.requirements.mutex p11]。由于 shared.write(new_data)
排序在 thr1 的 m.unlock()
之前,而 thr2 的 m.lock()
排序在 [=38= 之前], 通过追寻 [intro.races] 中的定义,我们看到 shared.write(new_data)
确实 发生在 shared.read()
.
之前
案例二
现在假设相反,在 m
的锁定顺序中,thr2 的锁定在 thr1 的解锁之前。由于同一互斥锁的锁定和解锁不能交错(这是互斥锁的全部要点,以提供互斥),m
上的锁总顺序必须如下:
thr2: m.lock()
thr2: m.unlock()
thr1: m.lock()
thr1: m.unlock()
这意味着 thr2 的 m.unlock()
与 thr1 的 m.lock()
同步。现在 updated.load()
排序在 thr2 m.unlock()
之前,而 thr1 m.lock()
排序在 [=14= 之前],因此 updated.load()
发生在 updated.store(true)
之前。通过 read-write 一致性 [intro.races p17],updated.load()
必须 而不是 从 updated.store(true)
中获取其值,而是从一些严格的早期副作用在 updated
的 修改顺序 中;大概它的初始化为 false
.
我们得出结论,在这种情况下,updated.load()
必须 return false
。但如果真是这样,那么 thr2 将永远不会首先尝试锁定互斥量。这是一个矛盾,所以情况二绝不能发生。
我有一个共享数据结构,它已经在内部与互斥体同步。我可以使用一个内存顺序放松的原子来表示变化吗?我在代码中的意思的一个非常简化的视图
线程 1
shared_conf.set("somekey","someval");
is_reconfigured.store(true, std::memory_order_relaxed);
线程 2
if (is_reconfigured.load(std::memory_order_relaxed)) {
inspect_shared_conf();
}
我能保证在 shared_map 中看到更新吗?;共享映射本身在内部将每个 write/read 调用与互斥锁
同步宽松的顺序意味着原子和外部操作的顺序只发生在特定原子对象上的操作(即使这样,编译器也可以在 program-defined 之外自由 re-order 它们命令)。因此,轻松存储与外部对象中的任何状态没有关系。所以宽松的负载不会与您的其他互斥锁同步。
acquire/release 语义的全部要点是允许原子控制其他内存的可见性。如果你想让一个原子加载意味着某物可用,它必须是一个获取并且它获取的值必须已经被释放。
您的示例代码可以运行,是的,您将看到更新。轻松的排序将为您提供正确的行为。也就是说,就性能而言,它实际上可能不是最佳的。
让我们看一个更具体的例子,其中互斥锁是显式的。
std::mutex m;
std::atomic<bool> updated;
foo shared;
void thr1() {
m.lock();
shared.write(new_data);
m.unlock();
updated.store(true, std::memory_order_relaxed);
}
void thr2() {
if (updated.load(std::memory_order_relaxed)) {
m.lock();
data = shared.read();
m.unlock();
}
}
通俗解释
m.lock()
是获取操作,m.unlock()
是释放操作。这意味着没有什么可以阻止 updated.store(true)
向上“浮动”到临界区,超过 m.unlock()
甚至 shared.write()
。乍一看这似乎很糟糕,因为 updated
标志的全部意义在于表示 shared.write()
已经完成。但是在那种情况下并没有实际的伤害发生,因为 thr1 仍然持有互斥量 m
,因此如果 thr2 开始尝试读取共享数据,它只会等到 thr1 删除它。
真正糟糕的是,如果 updated.store()
一直飘过 m.lock()
;然后 thr2 可能会看到 updated.load() == true
并在 thr1 之前获取互斥量。但是,由于获取语义 m.lock()
.
thr2
中可能存在一些相关问题(稍微复杂一些,因为它们必须是推测性的)但同样的事实再次拯救了我们:updated.load()
可以向下沉入关键部分,但没有完全超过它(因为 m.unlock()
是发布)。
但这是一个实例,其中 updated
操作的更强内存顺序虽然看起来更昂贵,但实际上可能会提高性能。如果 updated
中的值 true
过早可见,则 thr2 会在它已被 thr1 锁定时尝试锁定 m
,因此 thr2 在等待 [=19] 时将不得不阻塞=] 可用。但是如果改成updated.store(true, std::memory_order_release)
和updated.load(std::memory_order_acquire)
,那么updated
中的值true
只有在m
真正被thr1解锁后才能可见,所以m.lock()
在 thr2 中应该总是立即成功(忽略可能存在的任何其他线程的争用)。
证明
好的,这是一个非正式的解释,但我们知道在考虑内存排序时这些总是有风险的。让我们从C++内存模型的形式规则上给出一个证明。我将遵循 C++20 标准,因为我手头有它,但我认为与 C++17 相比没有任何重大的相关变化。有关此处使用的术语的定义,请参阅 [intro.races]。
我声称,如果 shared.read()
完全执行,那么 shared.write(new_data)
会在 之前发生,因此 write-read 一致性 [intro.races p18] shared.read()
会看到新的数据。
m
上的锁定和解锁操作完全有序 [thread.mutex.requirements.mutex p5]。考虑两种情况:thr1 的解锁先于 thr2 的锁定(情况 I),反之亦然(情况 II)。
案例一
如果在 m
的锁定顺序中 thr1 的解锁在 thr2 的锁定之前,则没有问题;它们 彼此同步 [thread.mutex.requirements.mutex p11]。由于 shared.write(new_data)
排序在 thr1 的 m.unlock()
之前,而 thr2 的 m.lock()
排序在 [=38= 之前], 通过追寻 [intro.races] 中的定义,我们看到 shared.write(new_data)
确实 发生在 shared.read()
.
案例二
现在假设相反,在 m
的锁定顺序中,thr2 的锁定在 thr1 的解锁之前。由于同一互斥锁的锁定和解锁不能交错(这是互斥锁的全部要点,以提供互斥),m
上的锁总顺序必须如下:
thr2: m.lock()
thr2: m.unlock()
thr1: m.lock()
thr1: m.unlock()
这意味着 thr2 的 m.unlock()
与 thr1 的 m.lock()
同步。现在 updated.load()
排序在 thr2 m.unlock()
之前,而 thr1 m.lock()
排序在 [=14= 之前],因此 updated.load()
发生在 updated.store(true)
之前。通过 read-write 一致性 [intro.races p17],updated.load()
必须 而不是 从 updated.store(true)
中获取其值,而是从一些严格的早期副作用在 updated
的 修改顺序 中;大概它的初始化为 false
.
我们得出结论,在这种情况下,updated.load()
必须 return false
。但如果真是这样,那么 thr2 将永远不会首先尝试锁定互斥量。这是一个矛盾,所以情况二绝不能发生。