C++ <mutex> 标头在执行并发时是不是使用硬件支持或纯粹是基于算法的解决方案

Posted

技术标签:

【中文标题】C++ <mutex> 标头在执行并发时是不是使用硬件支持或纯粹是基于算法的解决方案【英文标题】:Does C++ <mutex> header use hardware support when enforcing concurrency or is purely an algorithm based solutionC++ <mutex> 标头在执行并发时是否使用硬件支持或纯粹是基于算法的解决方案 【发布时间】:2020-03-11 18:37:25 【问题描述】:

C++ 互斥锁是如何在底层实现的?它是否仅使用 Decker、Peterson 或其他算法来强制互斥,或者它是否还使用硬件支持,例如中断广告比较和交换 (CAS)。

是否可以在没有任何硬件支持的情况下实现由互斥锁和条件变量组成的整个多线程库?

【问题讨论】:

互斥锁被称为昂贵的,因为它涉及到操作系统参与这个机制 但是是否可以在不涉及操作系统的情况下实现纯算法互斥? codereview.stackexchange.com/questions/28083/… 您可以研究一些实现来回答这些实现的问题。添加标准中的要求,以区分哪些是特定于实现的,哪些不是。 【参考方案1】:

标准要求有原子操作。不一定是CAS,但至少是交换。 std::atomic_flag 要求是真正的原子,虽然 CAS 对它来说是多余的,但简单的交换就可以了。

请注意,任何算法,如 Decker's、Peterson's 或其他至少需要原子加载和原子存储,如果加载和存储是非原子的,它们将无法工作。此外,非原子类型不强制执行内存排序,这些算法暗示了这一点。

实际上并不要求std::mutex 将基于操作系统调用,并且根本不需要操作系统调用。

理论上,std::mutex 只能旋转互斥体。

它也可能只阻塞互斥锁。

在实践中,一个好的实现会首先尝试使用原子处理阻塞,但是当它诉诸等待时,它会执行操作系统等待。

【讨论】:

感谢您的回答。所以彼得森和德克尔算法中使用的 turnflag 数组需要原子加载和存储,也就是说它们应该是原子布尔类型,对吗?我在网上看到这两种算法的许多实现,其中这两个变量没有定义为原子类型。 在实践中,boolvolatile bool 将是原子的,因为它们将在一个单独的部分中(一条指令加载/存储,它将以原子方式执行)。有问题的部分是内存顺序,它只适用于原子或显式内存栅栏。请参阅此处的示例,如果没有强制执行排序,即使在 x86 上 Peterson 锁也会被破坏:bartoszmilewski.com/2008/11/05/… 一般来说,仅基于 bool 读写的同步算法无论如何都是低效的(CPU 内核做了很多不必要的工作),因此它们不会在生产中使用。由于您发现的示例在实践中并未大量使用,因此可能不会发现任何问题。 另外一些例子可能是旧的。在 C++11 之前,C++ 中没有正式的多线程。非正式地,Peterson 锁实现可以假设不存在内存顺序问题,并且整个程序执行是顺序一致的。这是旧单核处理器的真实假设【参考方案2】:

它要求操作系统去做。

在可能会使用 pthreads (How does pthread implemented in linux kernel 3.2?) 的 Linux 上。

在 Windows 上,它使用 Windows API。您可以尝试向 Microsoft 询问该问题。

标准库实现不会直接访问硬件。这就是操作系统的用途。

【讨论】:

你说得对,这就是它通常的实现方式,但这不是必需的。而在没有原生线程的系统上,标准库实现当然可以提供。 这是内核线程和用户线程的区别吗?用户线程调用内核线程实现并发?那么现代语言中的所有多线程库都是基于用户线程并在底层调用一些内核线程库函数吗? @PeteBecker 对,但关键是如果他们需要这样的答案,OP 需要指定一个操作系统。实际上,这没有被两个(可能是三个)常见且有据可查的实现之一覆盖的可能性相当小。 inb4 一些关于嵌入式世界的评论;他们通常不会使用std::thread,而是使用 RTOS 或类似提供的任何东西。【参考方案3】:

在 Linux 上,当互斥体不竞争时,锁定和解锁仅使用原子指令在用户空间发生,例如比较和交换。在有争议的情况下,需要调用内核来等待互斥体,直到它被解锁(锁定时)或唤醒服务员(解锁时)。锁定用户空间互斥锁不会禁用中断。

Here is the source code for low-level mutex primitives in glibc,cmets 有教益:

/* Low-level locks use a combination of atomic operations (to acquire and
   release lock ownership) and futex operations (to block until the state
   of a lock changes).  A lock can be in one of three states:
   0:  not acquired,
   1:  acquired with no waiters; no other threads are blocked or about to block
       for changes to the lock state,
   >1: acquired, possibly with waiters; there may be other threads blocked or
       about to block for changes to the lock state.

   We expect that the common case is an uncontended lock, so we just need
   to transition the lock between states 0 and 1; releasing the lock does
   not need to wake any other blocked threads.  If the lock is contended
   and a thread decides to block using a futex operation, then this thread
   needs to first change the state to >1; if this state is observed during
   lock release, the releasing thread will wake one of the potentially
   blocked threads.

   Much of this code takes a 'private' parameter.  This may be:
   LLL_PRIVATE: lock only shared within a process
   LLL_SHARED:  lock may be shared across processes.

   Condition variables contain an optimization for broadcasts that requeues
   waiting threads on a lock's futex.  Therefore, there is a special
   variant of the locks (whose name contains "cond") that makes sure to
   always set the lock state to >1 and not just 1.

   Robust locks set the lock to the id of the owner.  This allows detection
   of the case where the owner exits without releasing the lock.  Flags are
   OR'd with the owner id to record additional information about lock state.
   Therefore the states of robust locks are:
    0: not acquired
   id: acquired (by user identified by id & FUTEX_TID_MASK)

   The following flags may be set in the robust lock value:
   FUTEX_WAITERS     - possibly has waiters
   FUTEX_OWNER_DIED  - owning user has exited without releasing the futex.  */

还有pthread_mutex_lock code。

互斥锁实现需要多个线程就共享变量的值达成一致——互斥锁,无论它是锁定的还是解锁的。这也称为consensus problem,仅使用原子加载和存储无法解决它,需要进行比较和交换。

【讨论】:

以上是关于C++ <mutex> 标头在执行并发时是不是使用硬件支持或纯粹是基于算法的解决方案的主要内容,如果未能解决你的问题,请参考以下文章

C++并发编程之二 在线程间共享数据

C++11 并发指南三(std::mutex 详解)

stdthread并发recursive_mutex 递归锁

C++ mutex详解

C++ 协议缓冲区:常量表达式中的临时非文字类型 'google::protobuf::internal::CallOnInitializedMutex <std::mutex>'

互斥量(mutex)