传递锁所有权

Passing lock ownership

提问人:André Caldas 提问时间:11/11/2023 最后编辑:UselessAndré Caldas 更新时间:11/17/2023 访问量:201

问:

在 RAII 中,我们有 std::unique_lockstd::scoped_lock。这两个是明确可移动的。RAII 对象在构造时被“锁定”,在被破坏时被“解锁”。因此,我认为绝对没有真正的理由禁止将“所有权”传递给不同的线程。如果它不正确或无用,人们可以对将unique_ptr传递到另一个线程说同样的话。

我想做这样的事情:

std::scoped_lock lock{mutex};
// Do some stuff.
// Decide to multithread...
auto lambda = [lock = std::move(lock)]
{
  // Do stuff with the lock.
};
std::thread{std::move(lambda)}.detach();

现在,我有两个问题:

  1. 我错过了什么?这没有意义吗?
  2. cppreference 对 std::scoped_lock的描述中,我没有看到任何关于不在其他线程中解锁的内容。所以,我想知道标准是否禁止我上面发布的代码。
C++ 多线程 互斥锁

评论

0赞 Hans Passant 11/11/2023
使用非递归互斥锁或信号量。
0赞 André Caldas 11/11/2023
@HansPassant:我正在使用非递归互斥锁。我实际上使用的是 std::shared_mutex。然而,该标准说这是未定义的行为。
0赞 Stephen C 11/11/2023
我已经标记了这个.在Java中有两种“锁”。一种是可转让的,另一种是不可转让的。不可转让的类型通常被称为“原始锁”或“互斥锁”......它实际上是所有对象的行为![c++]
0赞 André Caldas 11/11/2023
@Useless:这可能是一个错误,因为就“互斥锁”而言,人们在很大程度上等同于“原始指针”的用户。就像unique_ptr一样,我认为没有理由不允许互斥所有权被转移。与线程关联实际上是一种黑客攻击,从数学上讲是错误的,但仍然......大部分时间都有效。有一种“用户证明性”的虚假感觉。这就是“底层不发达机制”的作用。记住:这不仅仅是因为你没有看到它的“无用”的用处。;-)

答:

2赞 Useless 11/11/2023 #1

TL;博士

您绝对可以将从一个线程移动到另一个线程,只要底层 Lockable 支持它。但是,没有一个标准库互斥锁支持它。


因此,我认为绝对没有真正的理由禁止将“所有权”传递给不同的线程。

原则上,我也没有,有几个警告:

  1. 即使它有意义,如果标准是保守的或限制性的,或者希望为一些狡猾的平台特定优化留出实现空间,它也可能是 UB
  2. 某些特定的互斥锁类型(如递归互斥锁)可能需要存储当前线程 ID,因此无法处理此用例。

综上所述,我们可以检查标准:

[thread.req.lockable.general] 一般情况

1/ 执行代理是一个实体,例如线程,可以与其他执行代理并行执行工作。

[注 1:实现或用户可以引入其他类型的代理,例如进程或线程池任务。 — 尾注]

[注 2:一些可锁定对象是“代理遗忘的”,因为它们适用于任何执行代理模型,因为它们不确定或存储代理的 ID(例如,普通的旋转锁)。 — 尾注]

因此,听起来我们当然可以自由地使用我们自己的“执行代理”抽象,并且当我们的抽象从一个线程移动到另一个线程时,至少一些可锁定的类型应该可以正常工作。

不幸的是,没有一个标准互斥锁类型属于这一类:它们都指定它们必须由“拥有”互斥锁的线程解锁,拥有互斥锁的唯一方法是锁定它,并且没有描述转移所有权的机制。

但是,您可以基于信号量、自旋锁或其他任何未明确禁止这样做的类型编写自己的 Cpp17Lockable 类型,并认为自己正式受到该标准的祝福。

2赞 JaMiT 11/11/2023 #2

在 RAII 中,我们有 std::unique_lockstd::scoped_lock。这两个是明确可移动的。RAII 对象在构造时被“锁定”,在被破坏时被“解锁”。因此,我认为绝对没有真正的理由禁止将“所有权”传递给不同的线程。

是的,就 and 而言,将所有权转移到另一个线程是没有问题的。这是因为这些课程专注于一项任务——锁的 RAII。std::unique_lockstd::scoped_lock

这些类对互斥锁的操作方式一无所知。他们所需要的只是包装对象(例如互斥锁)是“(基本)可锁定的”——它具有成员函数,并且在某些情况下具有 .锁类确保对 的调用与对 的调用匹配,并且对返回的调用与对 的调用匹配。这些类不关心这些函数的效果是什么;重要的是在需要时调用它。lock()unlock()try_lock()lock()unlock()try_lock()trueunlock()unlock()

与其查看 RAII 包装器,不如查看正在包装的内容。如果要包装 std::mutex,则适用以下情况。

调用线程从成功调用 EITHER 或直到调用 时拥有mutexlocktry_lockunlock

这就是为什么您不能将 a 的所有权转移到另一个线程的原因。锁定的线程将拥有互斥锁,直到调用 。不仅直到(被某人)调用,而且直到该特定线程调用 .使用 时,不能将此任务委派给另一个线程。std::mutexstd::mutexunlockunlockunlockstd::mutex

另一个 Lockable 的行为可能不同,并允许在线程之间转移所有权,但事实并非如此。std::mutex

2赞 Solomon Slow 11/11/2023 #3

“不要这样做”的真正原因是,这是非常规的。如果你曾经期望其他程序员阅读你的代码——如果你曾经期望与他们合作,或者你曾经向其他程序员寻求帮助——那么如果每个人都说同一种语言,你会有更好的运气。

“互斥锁”不仅仅是某些编程语言中的类型名称。它是设计模式的名称。如果你在代码中使用了一种叫做“互斥锁”的东西,那么每个看到它的人都会立即期望你以某种方式使用它。他们不会喜欢和你一起工作或帮助你。

您要执行的操作有不同的名称。它被称为二进制信号量。它的作用几乎与互斥锁完全相同,只是,如果您将其锁定在一个线程中并将其锁定在另一个线程中,没有人会扬眉吐气。


* “Mutex”说,“我只是要在这里访问一些共享数据,我会尽可能快地处理它。
“信号量”说,“嘿!拿着我的啤酒,看着这个

0赞 James Kanze 11/17/2023 #4

为了补充前面的答案:必须从被调用的线程调用的原因是因为或其 Windows 等效项具有此约束。最后,这确实是互斥锁定义的一部分。unlocklockpthread_mutex_unlock