按照设计, std::mutex
既不可移动也不可复制。这意味着持有互斥锁的类 A
不会收到默认的移动构造函数。
我如何使这种类型 A
以线程安全的方式移动?
原文由 Jack Sabbath 发布,翻译遵循 CC BY-SA 4.0 许可协议
按照设计, std::mutex
既不可移动也不可复制。这意味着持有互斥锁的类 A
不会收到默认的移动构造函数。
我如何使这种类型 A
以线程安全的方式移动?
原文由 Jack Sabbath 发布,翻译遵循 CC BY-SA 4.0 许可协议
3 回答1.4k 阅读✓ 已解决
1 回答1.2k 阅读✓ 已解决
4 回答931 阅读
1 回答999 阅读
1 回答1k 阅读
1 回答788 阅读
1 回答889 阅读
让我们从一些代码开始:
我在其中放置了一些颇具暗示性的类型别名,我们不会在 C++11 中真正利用它们,但在 C++14 中变得更加有用。耐心点,我们会到达那里的。
你的问题归结为:
我们将从移动构造函数开始。
移动构造函数
请注意,成员
mutex
已mutable
。严格来说,这对于移动成员来说不是必需的,但我假设您也想要复制成员。如果不是这种情况,则无需制作互斥锁mutable
。构造
A
时,不需要加锁this->mut_
。但是您确实需要锁定您正在构建的对象的mut_
(移动或复制)。这可以这样做:请注意,我们必须首先默认构造
this
的成员,然后仅在a.mut_
被锁定后为其赋值。移动分配
移动赋值运算符要复杂得多,因为您不知道其他线程是否正在访问赋值表达式的 lhs 或 rhs。通常,您需要防范以下情况:
这是正确保护上述场景的移动赋值运算符:
请注意,必须使用
std::lock(m1, m2)
来锁定两个互斥锁,而不是一个接一个地锁定它们。如果你一个接一个地锁定它们,那么当两个线程以相反的顺序分配两个对象时,你可以得到一个死锁。std::lock
的重点是避免死锁。复制构造函数
你没有问副本成员,但我们现在不妨谈谈他们(如果不是你,有人会需要他们)。
复制构造函数看起来很像移动构造函数,除了使用
ReadLock
别名而不是WriteLock
。目前这两个别名std::unique_lock<std::mutex>
所以它并没有什么区别。但在 C++14 中,您可以选择这样说:
这 可能 是一种优化,但不是绝对的。您将不得不测量以确定它是否是。但是通过这种更改,可以同时 从 多个线程中的相同 rhs 复制构造。 C++11 解决方案强制您使此类线程连续,即使 rhs 没有被修改。
复制作业
为了完整起见,这里是复制赋值运算符,在阅读完其他所有内容后应该是相当不言自明的:
等等。
如果您希望多个线程能够一次调用它们,则任何其他访问
A
状态的成员或自由函数也需要受到保护。例如,这里是swap
:请注意,如果您只依赖
std::swap
完成这项工作,则锁定的粒度会错误,锁定和解锁在std::swap
将在内部执行的三个动作之间。实际上,考虑
swap
可以让您深入了解您可能需要为“线程安全”提供的 APIA
,这通常与“非线程”不同-safe” API,因为“锁定粒度”问题。还要注意防止“自我交换”的必要性。 “自我交换”应该是无操作的。如果没有自检,就会递归地锁定同一个互斥锁。这也可以在没有自检的情况下通过使用
std::recursive_mutex
forMutexType
来解决。更新
在下面的评论中,Yakk 非常不满意必须在复制和移动构造函数中默认构造东西(他有一点)。如果你对这个问题感觉足够强烈,以至于你愿意在上面花费内存,你可以像这样避免它:
糟糕,在我有机会完成此更新之前,Yakk 删除了他的评论。但他在推动这个问题并为这个答案找到解决方案方面值得称赞。
更新 2
dyp 提出了这个好建议: