C++ std::condition_variable 条件变量类探索:解锁条件变量的底层原理

简介: C++ std::condition_variable 条件变量类探索:解锁条件变量的底层原理

简介

C++ 标准库中的 std::condition_variable 类提供了一些接口,用于线程同步和条件等待。按照功能分类,它们分为以下几类:

  1. 等待(Wait):
  • wait(std::unique_lock& lock): 当前线程等待,直到条件变量被通知。在等待期间,锁会被解锁。
  • wait(std::unique_lock& lock, Predicate pred): 当前线程等待,直到条件变量被通知且谓词 pred 返回 true。在等待期间,锁会被解锁。
  1. 带超时的等待(Timed wait):
  • wait_for(std::unique_lock& lock, const std::chrono::duration& rel_time): 当前线程等待,直到条件变量被通知或超时。在等待期间,锁会被解锁。
  • wait_for(std::unique_lock& lock, const std::chrono::duration& rel_time, Predicate pred): 当前线程等待,直到条件变量被通知且谓词 pred 返回 true 或超时。在等待期间,锁会被解锁。
  • wait_until(std::unique_lock& lock, const std::chrono::time_point& abs_time): 当前线程等待,直到条件变量被通知或达到指定的绝对时间点。在等待期间,锁会被解锁。
  • wait_until(std::unique_lock& lock, const std::chrono::time_point& abs_time, Predicate pred): 当前线程等待,直到条件变量被通知且谓词 pred 返回 true 或达到指定的绝对时间点。在等待期间,锁会被解锁。
  1. 通知(Notify):
  • notify_one(): 随机唤醒一个等待中的线程。
  • notify_all(): 唤醒所有等待中的线程。

以上就是 std::condition_variable 类提供的所有接口。这些接口用于在多线程环境中实现同步和条件等待,以确保线程安全地访问共享资源。

等待(Wait)

std::condition_variable::wait() 函数有两个重载版本:一个只接受 std::unique_lock 参数,另一个接受 std::unique_lock 和一个谓词(predicate)参数。在我们这个例子中,我们使用了第二个重载版本,它接受一个谓词参数。

当我们调用带有谓词参数的 wait() 方法时,这个谓词将用于检查条件是否满足。在内部,wait() 函数会在一个循环中执行以下操作:

  1. 检查谓词是否返回 true。如果返回 true,则跳出循环,线程继续执行。
  2. 如果谓词返回 false,线程进入等待状态,等待条件变量被通知。
  3. 当条件变量被通知(通过 notify_one()notify_all())时,线程从等待状态唤醒,并重新检查谓词。

wait(std::unique_lockstd::mutex& lock)

wait(std::unique_lock& lock)std::condition_variable 类中的一个成员函数,它用于阻塞当前线程,直到条件变量被通知。在等待期间,传递给 wait() 的互斥锁会被解锁,从而允许其他线程锁定该互斥锁并访问共享资源。当条件变量被通知后,锁会重新锁定,线程会继续执行。

以下是 wait() 方法的基本使用方法:

std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
    // ... do some work ...
    {
        std::unique_lock<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();
}
void main_thread() {
    std::unique_lock<std::mutex> lock(mtx);
    while (!ready) {
        cv.wait(lock);
    }
    // ... continue processing when ready ...
}

在这个示例中,主线程等待工作线程完成任务并将 ready 变量设为 true。主线程在 cv.wait(lock) 处阻塞,直到工作线程调用 cv.notify_one()

底层逻辑:

  1. 当前线程调用 wait() 函数时,会将传入的 std::unique_lock 对象解锁,释放对互斥锁的控制。
  2. 当前线程进入阻塞状态,等待条件变量的通知。在这个阶段,其他线程可以获取互斥锁并访问共享资源。
  3. 当其他线程调用 notify_one()notify_all() 时,至少一个正在等待的线程(在本例中是主线程)会被唤醒。
  4. 当前线程在唤醒后会尝试重新获取传入的 std::unique_lock 对象(即重新锁定互斥锁)。
  5. 互斥锁重新锁定后,当前线程会从 wait() 函数返回,继续执行后续代码。

需要注意的是,虽然条件变量的通知机制可以唤醒等待的线程,但在某些情况下(例如虚假唤醒),线程可能会在条件未满足的情况下被唤醒。因此,在使用 wait() 时,通常需要将其放在一个循环中,并在循环条件中检查等待的条件是否满足。这样可以确保线程只在条件真正满足时才会继续执行。

wait(std::unique_lockstd::mutex& lock, Predicate pred)

当线程被唤醒时,wait() 函数会执行传递给它的第二个参数(谓词)。这个谓词用于检查条件是否满足。只有当谓词返回 true 时,线程才会继续执行。否则,线程会再次进入等待状态。

谓词可以是任何可调用对象,包括函数、成员函数、静态成员函数、lambda 表达式等。关键是它必须能够被调用,并且返回一个布尔值(truefalse)来表示条件是否满足。

以下是一些可以作为谓词的示例:

  1. Lambda 表达式:
m_cond_var.wait(lock, [this] { return m_signal_received; });
  1. 成员函数(使用 std::bind() 绑定):
bool is_signal_received() const { return m_signal_received; }
// ...
m_cond_var.wait(lock, std::bind(&PlayMangent::is_signal_received, this));
  1. 静态成员函数:
static bool is_signal_received_static(const PlayMangent* self) { return self->m_signal_received; }
// ...
m_cond_var.wait(lock, std::bind(&PlayMangent::is_signal_received_static, this));
  1. 全局函数:
bool is_signal_received_global(const PlayMangent* self) { return self->m_signal_received; }
// ...
m_cond_var.wait(lock, std::bind(is_signal_received_global, this));
  1. 函数对象(仿函数,Functor):
class SignalChecker {
public:
    explicit SignalChecker(const PlayMangent* play_mangent) : m_play_mangent(play_mangent) {}
    bool operator()() const { return m_play_mangent->m_signal_received; }
private:
    const PlayMangent* m_play_mangent;
};
// ...
SignalChecker checker(this);
m_cond_var.wait(lock, checker);

总之,谓词可以是任何可调用对象,只要它能够返回一个表示条件是否满足的布尔值即可。您可以根据您的需求和编程风格选择合适的谓词类型。

Predicate

在 C++ 中,Predicate 是一个泛指,用于描述可调用对象(例如函数、函数指针、Lambda 表达式、仿函数等),这些对象接受一个或多个参数,返回一个布尔值(truefalse)。Predicate 在许多 C++ 标准库中的算法和函数中使用,作为参数来表示条件。

当谈论 std::condition_variable::wait() 方法及其重载版本时,Predicate 是一个可调用对象,它不接受任何参数并返回一个布尔值。这个谓词用于在调用 wait() 方法时检查等待的条件是否满足。如果谓词返回 true,则线程继续执行;如果谓词返回 false,则线程继续等待。

以下是一个简单的示例,使用 Lambda 表达式作为谓词:

std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void main_thread() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });
    // ... continue processing when ready ...
}

在这个示例中,谓词是一个 Lambda 表达式 [] { return ready; },它检查 ready 变量是否为 true。当 ready 变为 true 时,线程从 wait() 返回并继续执行。

虚假唤醒

虚假唤醒是指一个线程在没有收到条件变量通知的情况下仍然从等待状态唤醒。虚假唤醒可能发生在任何平台,包括 ARM 系统。虽然虚假唤醒在实际中相对罕见,但仍然需要考虑它们以确保代码的正确性。

虚假唤醒的具体原因可能因系统和库的实现而异。以下是一些可能导致虚假唤醒的原因:

  1. 操作系统实现:操作系统在实现线程调度时,可能会因为内部调度策略、优化或者错误而导致虚假唤醒。在某些情况下,操作系统可能允许条件变量在没有实际通知的情况下唤醒线程,以确保系统的活跃度和响应能力。
  2. 信号处理:如果在等待条件变量时,线程收到一个信号(例如,由于操作系统中断或其他线程发送的信号),线程可能会因为处理信号而唤醒。在这种情况下,唤醒并不是由于条件变量的通知,而是由于信号处理导致的。
  3. 库实现:虚假唤醒有时可能是库实现的副作用。例如,C++ 标准库中的 std::condition_variable 的实现可能会因为内部实现细节、优化或错误而导致虚假唤醒。

为了应对虚假唤醒,我们需要在等待条件变量时使用一个谓词(predicate)。谓词是一个函数或者 lambda 表达式,它返回一个布尔值。当谓词为 true 时,线程将从等待状态唤醒并继续执行。这样可以确保线程只在满足特定条件时才从等待状态唤醒,从而避免虚假唤醒带来的问题。

当一个线程在等待条件变量时,它实际上是在等待满足某个特定条件。在 C++ 中,std::condition_variablewait() 函数有两种重载形式:一种只接受一个互斥锁(std::unique_lock),另一种接受一个互斥锁和一个谓词。如果你使用的是第一种重载形式,那么线程在被唤醒后会继续执行,而不会检查条件是否满足。在这种情况下,如果发生虚假唤醒,线程将继续执行,即使条件未满足。

为了避免这个问题,我们应该使用第二种重载形式,也就是带有谓词的 wait() 函数。谓词是一个返回布尔值的函数或 lambda 表达式,用于检查条件是否满足。当谓词为 true 时,线程会从等待状态唤醒并继续执行。在这种情况下,即使发生虚假唤醒,线程仍然会检查谓词。如果谓词为 false,线程将重新进入等待状态,直到条件满足(谓词返回 true)。

所以,当我们提到虚假唤醒时,我们实际上是指线程在没有收到条件变量通知的情况下从等待状态唤醒。如果我们使用带有谓词的 wait() 函数,那么即使发生虚假唤醒,线程仍然会检查条件是否满足,并在条件未满足时重新进入等待状态。这就是为什么我们需要使用带有谓词的 wait() 函数来保护我们的代码免受虚假唤醒的影响。

在之前给出的示例代码中,我们使用了一个谓词:

m_cond_var.wait(lock, [this] { return m_signal_received; });

这个谓词检查 m_signal_received 的值。只有当 m_signal_receivedtrue 时,线程才会从等待状态唤醒并继续执行。这样,即使发生虚假唤醒,线程仍然会检查条件是否满足,从而避免因虚假唤醒导致的问题。

std::condition_variable 在 Linux 系统下通常是基于 pthread 库实现的。因此,它的底层实际上使用了 POSIX 线程库(pthread)提供的条件变量函数,例如 pthread_cond_initpthread_cond_waitpthread_cond_signal 等。

POSIX 线程库中的条件变量也可能会遇到虚假唤醒的问题。在使用 pthread_cond_wait 等待条件变量时,我们通常需要将其放在一个循环中,检查条件是否满足。例如:

pthread_mutex_lock(&mutex);
while (!condition_is_met) {
    pthread_cond_wait(&cond_var, &mutex);
}
pthread_mutex_unlock(&mutex);

在这个示例中,我们将 pthread_cond_wait 放在一个循环中,并检查 condition_is_met 是否为 true。只有当条件满足时(condition_is_met == true),线程才会继续执行。这样,即使发生虚假唤醒,线程仍然会检查条件是否满足,并在条件不满足时重新进入等待状态。这与 C++ std::condition_variable 使用谓词的方法类似。

所以,POSIX 线程库中的条件变量也可能会遇到虚假唤醒的问题,但是我们可以通过在循环中检查条件是否满足的方法来避免受到虚假唤醒的影响。

带超时的等待(Timed wait)

std::condition_variable 类提供了带超时的等待接口,它们允许在等待条件变量时设置超时限制。这些接口有两种形式:wait_forwait_untilwait_for 使用相对时间等待,而 wait_until 使用绝对时间点等待。每种形式都有两个重载版本:一个只接受超时时间,另一个还接受谓词(Predicate)。

  1. wait_for(std::unique_lock& lock, const std::chrono::duration& rel_time)
    当前线程等待条件变量被通知或超时。在等待期间,互斥锁会被解锁。如果在超时时间内条件变量未被通知,线程将因超时而唤醒。
  2. wait_for(std::unique_lock& lock, const std::chrono::duration& rel_time, Predicate pred)
    当前线程等待条件变量被通知且谓词 pred 返回 true,或者超时。在等待期间,互斥锁会被解锁。如果在超时时间内条件变量未被通知或谓词未返回 true,线程将因超时而唤醒。
  3. wait_until(std::unique_lock& lock, const std::chrono::time_point& abs_time)
    当前线程等待条件变量被通知或达到指定的绝对时间点。在等待期间,互斥锁会被解锁。如果在指定的绝对时间点之前条件变量未被通知,线程将因超时而唤醒。
  4. wait_until(std::unique_lock& lock, const std::chrono::time_point& abs_time, Predicate pred)
    当前线程等待条件变量被通知且谓词 pred 返回 true,或者达到指定的绝对时间点。在等待期间,互斥锁会被解锁。如果在指定的绝对时间点之前条件变量未被通知或谓词未返回 true,线程将因超时而唤醒。

底层原理:

  1. 当调用超时等待接口时,当前线程会解锁传入的互斥锁,允许其他线程获取锁并访问共享资源。
  2. 当前线程进入阻塞状态,等待条件变量的通知或超时。
  3. 当其他线程调用 notify_one()notify_all() 时,至少一个正在等待的线程(可能是当前线程)会被唤醒。如果超时发生,当前线程也会被唤醒。
  4. 当前线程在唤醒后会尝试重新获取传入的互斥锁。
  5. 对于带谓词的重载版本,唤醒后的线程会检查谓词是否满足(返回 true)。如果谓词返回 true,线程会继续执行;如果谓词返回 false,线程将继续等待,直到超时或谓词满足。

注意:在所有超时等待接口中,如果因超时而唤醒,线程需要检查条件是否满足以正确处理超时情况。如果使用带谓词的重载版本,谓词会自动进行这个检查。

示例:

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
void worker_thread() {
    std::unique_lock<std::mutex> lock(mtx);
    bool timeout_occurred = cv.wait_for(lock, std::chrono::seconds(5), [&] { return data_ready; });
    if (timeout_occurred) {
        std::cout << "Timeout occurred" << std::endl;
    } else {
        std::cout << "Data is ready" << std::endl;
    }
}
void main_thread() {
    // ... do some work ...
    // Notify worker_thread that data is ready
    {
        std::lock_guard<std::mutex> lock(mtx);
        data_ready = true;
    }
    cv.notify_one();
}

在这个示例中,worker_thread 使用 wait_for 方法等待最多 5 秒。如果在 5 秒内 data_ready 变量变为 true,线程继续执行;否则,线程因超时而唤醒,并输出 “Timeout occurred”。

超时等待的干扰因素

超时等待受到一些因素的影响,例如调度器策略、系统负载、硬件性能等。这些因素可能导致实际等待时间与设定的超时时间有所不同。在高负载或资源受限的情况下,线程可能需要等待比设定的超时时间更长的时间才能被唤醒。

关于系统时间的影响,这取决于使用的超时等待接口和底层实现。在C++11及其后续版本中,std::chrono 提供了不同类型的时钟,它们对系统时间变化的敏感程度不同。

  1. std::chrono::system_clock:这个时钟是受系统时间影响的墙上时钟(wall-clock time)。如果在等待期间系统时间发生变化(例如,由于手动更改时间或自动同步时间),这可能会影响到 wait_until 方法使用 system_clock 的超时行为。但是,这对 wait_for 方法没有影响,因为它使用的是相对时间。
  2. std::chrono::steady_clock:这个时钟提供单调递增的时间,不受系统时间的影响。它通常用于测量时间间隔。当使用 steady_clock 时,wait_until 方法不会受到系统时间变化的影响。

建议在涉及超时的场景中使用 std::chrono::steady_clock,以避免受到系统时间变化的影响。然而,请注意,C++标准库中的默认实现可能因平台而异。为了确保使用稳定的时钟,请显式指定 std::chrono::steady_clock

通知(Notify)

std::condition_variable 的通知(Notify)接口用于唤醒等待中的线程。这些方法通常在条件满足时调用,以唤醒正在等待条件变量的一个或多个线程。通知接口有两个方法:

  1. notify_one(): 此方法随机唤醒一个等待中的线程。如果有多个线程正在等待条件变量,只有一个会被选择并唤醒。选择哪个线程是不确定的。在实际应用中,为了避免竞争,我们通常在已经持有互斥锁的情况下调用此方法。然而,调用 notify_one() 本身并不需要持有互斥锁。
  2. notify_all(): 此方法唤醒所有等待中的线程。如果有多个线程正在等待条件变量,它们都将被唤醒并开始执行。这个方法在需要通知所有等待线程的场景中很有用。与 notify_one() 类似,我们通常在已经持有互斥锁的情况下调用此方法。然而,调用 notify_all() 本身并不需要持有互斥锁。

底层原理:

C++ std::condition_variable 的底层实现通常依赖于操作系统提供的原生条件变量支持,例如在 POSIX 系统中使用 pthread_cond_signal()pthread_cond_broadcast() 函数,或在 Windows 系统中使用 WakeConditionVariable()WakeAllConditionVariable() 函数。

当调用 notify_one()notify_all() 时,底层实现会通知操作系统去唤醒一个或所有正在等待条件变量的线程。这个过程通常需要涉及到操作系统调度器,以便在唤醒的线程获得 CPU 时间片并开始执行。

需要注意的是,在调用通知方法之后,线程不会立即开始执行,因为它们还需要重新获取互斥锁。当一个线程被唤醒时,它会尝试重新获取互斥锁。一旦锁被成功获取,线程将继续执行。如果有多个线程同时被唤醒,它们将竞争互斥锁,而只有一个线程能够继续执行,其他线程将继续等待锁。


目录
相关文章
|
5天前
|
存储 编译器 C语言
c++的学习之路:5、类和对象(1)
c++的学习之路:5、类和对象(1)
19 0
|
5天前
|
C++
c++的学习之路:7、类和对象(3)
c++的学习之路:7、类和对象(3)
19 0
|
4天前
|
设计模式 Java C++
【C++高阶(八)】单例模式&特殊类的设计
【C++高阶(八)】单例模式&特殊类的设计
|
4天前
|
设计模式 C语言 C++
【C++进阶(六)】STL大法--栈和队列深度剖析&优先级队列&适配器原理
【C++进阶(六)】STL大法--栈和队列深度剖析&优先级队列&适配器原理
|
4天前
|
编译器 C++
【C++基础(八)】类和对象(下)--初始化列表,友元,匿名对象
【C++基础(八)】类和对象(下)--初始化列表,友元,匿名对象
|
4天前
|
存储 C++
C++底层原理
C++底层原理
13 0
|
8天前
|
存储 安全 C语言
【C++】string类
【C++】string类
|
30天前
|
存储 C++ 容器
C++入门指南:string类文档详细解析(非常经典,建议收藏)
C++入门指南:string类文档详细解析(非常经典,建议收藏)
38 0
|
存储 编译器 Linux
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
|
10天前
|
编译器 C++
标准库中的string类(上)——“C++”
标准库中的string类(上)——“C++”