noexcept 交换和移动具有互斥锁的类

Posted

技术标签:

【中文标题】noexcept 交换和移动具有互斥锁的类【英文标题】:noexcept swap and move for classes with mutexes 【发布时间】:2020-09-26 18:44:04 【问题描述】:

一般来说,声明一个交换并移动 noexcept 是一个很好的做法,因为这样可以提供一些异常保证。 同时编写一个线程安全的类通常意味着添加一个互斥锁来保护内部资源免受竞争。 如果我想为这样的类实现交换函数,直接的解决方案是以安全的方式锁定交换的两个参数的资源,然后执行资源交换,例如,在这个问题的答案中明确回答: Implementing swap for class with std::mutex.

这种算法的问题在于互斥锁不是noexcept,因此swap严格来说不能是noexcept。有没有一种解决方案可以用互斥锁安全地交换一个类的两个对象?

我想到的唯一可能性是将资源存储为句柄,以便交换成为可以原子完成的简单指针交换。 否则,人们可能会将锁定异常视为不可恢复的错误,无论如何都应该终止程序,但这种解决方案感觉就像是一种将灰尘放在地毯下的方法。

编辑: 正如在 cmets 中得出的那样,我知道互斥体抛出的异常不是任意,但是问题可以这样改写:

是否有可靠的做法来限制互斥锁可能引发的情况,而这实际上是一个不可恢复的操作系统问题? 我想到的是在交换算法中检查要交换的两个对象是否不相同。这是一个明显的死锁情况,在最好的情况下会触发异常,但可以轻松检查。 是否有其他类似的触发器可以安全地检查以使交换函数变得健壮并且几乎没有任何重要的情况?

【问题讨论】:

对我来说锁定移动似乎没有意义,因为一旦移动结束,线程可能会在重新分配之前开始使用移动的值。所以在这种情况下,无论如何你都会明确地锁定移动和赋值 @AlexLarionov 当然,移动共享对象是不安全的,但这是用户问题。我不确定移动时不锁定是否会使情况变得更好。无论如何,交换的问题仍然存在 作为最后的手段,您可以在锁定时捕获所有异常。尽管执行时间可能变得不可预测 是的,但这意味着要么跳过交换,要么在未锁定的资源上执行交换,这可能使其处于未定义状态。我想我宁愿终止而不是默默地制造这样一个潜在的混乱 @Triskeldeian 我认为std::mutex 大多数实现在锁定已锁定的互斥锁或使用未初始化的互斥锁时尝试返回错误。反正这些都是UB。我认为locknoexcept(false) 的意义在于发出程序错误的信号,而不是处理它们。想想~unique_lock:它是noexcept(true),但调用unlocknoexcept(false)。如果出现故障,整个程序就会终止——这就是大多数互斥锁的使用方式。 【参考方案1】:

在 POSIX 系统上,std::mutexpthread_mutex_t 的一个瘦包装器是很常见的,在以下情况下锁定和解锁功能可能会失败:

正在尝试获取已拥有的锁 互斥对象未初始化或已被销毁

以上两个都是 C++ 中的 UB,甚至不保证被 POSIX 返回。在 Windows 上,如果 std::mutexSRWLOCK 的包装器,则两者都是 UB。

因此,似乎允许lockunlock 函数抛出的主要目的是通知程序中的错误,而不是让程序员期待并处理它们。

推荐的锁定模式证实了这一点:析构函数~unique_locknoexcept(true),但应该调用unlock,即noexcept(false)。这意味着如果unlock函数抛出异常,整个程序将被std::terminate终止。

标准也提到了这一点:

成员报告的错误代码的错误条件(如果有) 互斥类型的功能应为:

(4.1) — resource_unavailable_try_again — 如果有任何本机句柄类型 操纵不可用。

(4.2) — operation_not_permitted — 如果线程没有 执行操作的权限。

(4.3) — invalid_argument — 如果任何本机句柄类型被操作为 部分互斥体构造不正确

理论上您可能会遇到operation_not_permitted 错误,但标准中并未真正定义这种情况。

因此,除非您在程序中导致与 std::mutex 使用相关的 UB 或在某些特定于操作系统的场景中使用互斥锁,否则 lockunlock质量 实现永远不应该抛出。

在常见的实现中,至少有一个可能是低质量的:std::mutex 在旧版本的 Windows 中实现在 CRITICAL_SECTION 之上(我认为是 Windows XP 和更早版本)在延迟分配失败后可能会抛出争用期间的内部事件。另一方面,甚至更早的版本在初始化期间分配此事件以防止以后失败,因此std::mutex::mutex 构造函数可能需要在那里抛出(即使它在标准中是noexcept(true))。

【讨论】:

以上是关于noexcept 交换和移动具有互斥锁的类的主要内容,如果未能解决你的问题,请参考以下文章

TencentOS tiny深度源码分析—— 互斥锁

互斥锁的示例

linux下信号量和互斥锁的区别

互斥锁,自旋锁,原子操作原理和实现

线程同步互斥锁和读写锁的区别和各自适用场景

高效编程之互斥锁和自旋锁的一些知识