条件变量底层实现原理

在并发编程中,条件变量用来控制线程之间的同步,总的来说条件变量相关的操作主要有两个——wait和notify,在写博客时我有这样一些疑问:

1.wait操作是如何实现的,它是如何让线程进入睡眠的

2.notiy操作是如何做到通知正在等待的线程的,并且如何做到notify单个线程和notify所有线程的,如果是notify所有线程,那么这些被唤醒的线程可以并行执行吗(多核情况下)

3.为什么在调用wait操作时,需要传入mutex互斥锁,而notify时却不用。

4.c++中的条件变量为什么要配合unique_lock<mutex>锁而不是std::mutex使用或者lock_guard<mutex>

本文从以下几个方面对条件变量进行深入的理解

1.C++中的condition_variable实现原理

2.linux pthread库中的pthread condition variable实现原理

3.从操作系统源码的角度理解条件变量是如何实现的

4.有哪些错误的条件变量实现方式

条件变量使用的例子

例1

参考:C++ std::condition_variable 条件变量类探索:解锁条件变量的底层原理_std 条件变量-优快云博客

include<iostream>// std::coutinclude<thread>// std::threadinclude<mutex>// std::mutex, std::unique_lockinclude<condition_variable>// std::condition_variableusingnamespace std;
 
mutex mtx;                          // 互斥量
condition_variable cv;              // 条件变量bool ready = false;                 // 标志量voidprint_id(int id){unique_lock<mutex> lck(mtx);    // 上锁while (!ready) {
        cv.wait(lck);               // 线程等待直到被唤醒(释放锁 + 等待,唤醒,在函数返回之前重新上锁)
    }
    cout << "thread " << id << '\n';
}
 
voidgo(){unique_lock<mutex> lck(mtx);    // 上锁
    ready = true;
    cv.notify_all();                // 唤醒所有正在等待(挂起)的线程(在这里面要释放锁,为了在wait函数返回之前能成功的重新上锁)
}
 
intmain(){
    thread threads[10];for (int i = 0; i<10; ++i) {
        threads[i] = thread(print_id, i);
    }
    cout << "10 threads ready to race...\n";go();
 for (auto& th : threads) {
        th.join();
    }
 return0;
}

从这个例子可以看出,在调用notify和wait时,都需要进行加锁操作,所以mutex互斥锁锁住的其实是ready这个变量。到这里第二个问题中的“被唤醒的线程能否并行执行”也就有答案了,答案是不能,因为每个线程是需要获得互斥锁unique_lock<mutex>的。

对于第四个问题,答案是出于安全考虑,主要是防止程序员忘记对std::mutex进行unlock操作,特别是在一些异常处理中。至于为什么不用lock_guard[std::mutex](std::mutex),这个在后面介绍wait底层实现时说明

C++中的condition_variable实现原理

参考:C++ std::condition_variable 条件变量类探索:解锁条件变量的底层原理_std 条件变量-优快云博客

c++ 11引入了条件变量,在gcc中的实现位于libstdc++-v3/include/std/condition_variable路径下(为了方面阅读,本文对源码略有修改)

class condition_variable
  {
    __condvar _M_cond;

  public:
    typedef __gthread_cond_t *native_handle_type;

    condition_variable() noexcept;
    ~condition_variable() noexcept;

    condition_variable(const condition_variable &) = delete;
    condition_variable &operator=(const condition_variable &) = delete;

    void
    notify_one() noexcept;

    void
    notify_all() noexcept;
    void
    wait(unique_lock<mutex> &__lock) {
        _M_cond.wait(*__lock.mutex());
    }

    template <typename _Predicate>
    void
    wait(unique_lock<mutex> &__lock, _Predicate __p)
    {
      while (!__p())
        wait(__lock);
    }
  }

可以看到wait/notify操作实际上是调用__condvar的接口,可以看到wait操作中获取了互斥量,从而在底层调用互斥量的lock/unlock接口,而lock_guard属于RAII 类,在构造函数中加锁,在析构函数中解锁,除此之外无法对其管理的互斥量进行lock/unlock接口。这里解答了第四个问题,由于wait底层操作中需要对互斥量进行lock/unlock操作,lock_guard并不满足这样的需求。

__condvar实际上是对glibc中pthread condition variable的一层包装,如下:

class __condvar
  {
  public:
   ...
    void
    wait(mutex &__m)
    {
      int __e __attribute__((__unused__)) = __gthread_cond_wait(&_M_cond, __m.native_handle());
      __glibcxx_assert(__e == 0);
    }

    void
    wait_until(mutex &__m, timespec &__abs_time)
    {
      __gthread_cond_timedwait(&_M_cond, __m.native_handle(), &__abs_time);
    }

    ...
    void
    notify_one() noexcept
    {
      int __e __attribute__((__unused__)) = __gthread_cond_signal(&_M_cond);
      __glibcxx_assert(__e == 0);
    }

    void
    notify_all() noexcept
    {
      int __e __attribute__((__unused__)) = __gthread_cond_broadcast(&_M_cond);
      __glibcxx_assert(__e == 0);
    }

  protected:
#ifdef __GTHREAD_COND_INIT
    pthread_cond_t _M_cond = __GTHREAD_COND_INIT;
#else
    pthread_cond_t _M_cond;
#endif
  };

可以看到,wait/notify操作底层主要是调用glibc的pthread condition variable接口。

glibc中的pthread condition variable实现原理

在网上找了一篇博客,从源码角度分析了条件变量的实现原理,细节暂时看不懂,以后再看吧深入了解glibc的条件变量_牛客网

看了这篇博客可以解答第三个问题,对于wait操作,其底层实现是需要对互斥量进行lock/unlock操作的,因此需要传入unique_lock

TODO

### Java 中 `synchronized` 关键字与 `ReentrantLock` 的底层实现原理 #### 一、`synchronized` 的底层实现 `synchronized` 是一种内置锁机制,由 JVM 提供支持。它的核心依赖于 **Monitor(监视器)** 和 **Object Header(对象头)**。 - 每个 Java 对象都有一个关联的 Monitor 对象[^3]。当线程试图进入被 `synchronized` 修饰的方法或代码块时,它实际上是在尝试获取该对象的 Monitor 锁。 - 如果多个线程同时请求同一个对象的 Monitor 锁,则只有其中一个线程能够成功获得锁并继续执行,其他线程则会被阻塞,直到当前持有锁的线程释放锁为止[^2]。 在 JVM 实现层面,`synchronized` 的具体过程如下: 1. 当线程首次尝试访问某个同步区域时,JVM 会在对象头中标记锁的状态。 2. 若锁未被占用,则直接标记为已锁定状态,并允许线程进入同步区。 3. 若锁已被占用,则当前线程将加入到等待队列中,直至锁可用。 4. 同步结束后,线程会自动释放锁,使下一个排队的线程有机会获取锁。 此外,`synchronized` 还利用了 CAS(Compare And Swap)、自旋锁以及轻量级锁升级等优化技术来提升性能[^4]。 --- #### 二、`ReentrantLock` 的底层实现 `ReentrantLock` 是基于 AQS(AbstractQueuedSynchronizer)框架实现的一种可重入锁。AQS 使用了一个 FIFO 队列结构管理线程间的竞争关系,并通过原子操作维护共享资源的状态。 以下是 `ReentrantLock` 的主要工作流程: 1. 调用 `lock()` 方法时,线程首先尝试以独占方式获取锁。如果此时没有其他线程占有锁,则当前线程可以直接获取锁并设置其拥有者。 2. 如果锁已经被另一个线程占据,则调用方线程会被挂起,并放入 AQS 维护的 CLH(Craig, Landin, and Hagersten Locks)队列中等待。 3. 解锁时,持有锁的线程调用 `unlock()` 方法,这会导致唤醒队列中最前面的一个等待线程去重新争夺锁[^1]。 值得注意的是,`ReentrantLock` 支持公平性和非公平性两种模式的选择,默认是非公平锁。这种灵活性使得开发者可以根据实际需求调整程序行为。 --- #### 三、两者的对比分析 | 特性 | `synchronized` | `ReentrantLock` | |-------------------------|----------------------------------------|---------------------------------------| | 是否需要显式解锁 | 不需要 | 需要 | | 性能优化 | 自动完成 | 更灵活可控 | | 条件变量支持 | 单一条件 | 多个独立的 Condition | | 可中断特性 | 不支持 | 支持 | 尽管两者都能满足基本的并发控制需求,但在某些场景下,比如需要更复杂的锁功能或者更高的定制化程度时,推荐使用 `ReentrantLock`。 --- ```java // ReentrantLock 示例代码 import java.util.concurrent.locks.ReentrantLock; public class Counter { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // 加锁 try { count++; } finally { lock.unlock(); // 确保最终总是解锁 } } public int getCount() { return count; } } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值