突破编程_C++_C++11新特性(多线程编程的条件变量 condition_variable )

1 std::condition_variable 的基础概念

1.1 条件变量的作用与重要性

在线程开发中,条件变量扮演着至关重要的角色。它是一种同步机制,主要用于协调线程之间的操作,特别是在等待某些特定条件成立时。通过使用条件变量,开发者能够更高效地管理线程的执行,提高程序的性能和响应速度。

条件变量的主要作用体现在以下几个方面:

  • 线程同步与协调:条件变量允许线程在等待某些特定条件时被阻塞,直到其他线程在满足这些条件时通知它们。这种机制确保了线程间的有序执行,避免了线程间的无序竞争和冲突。
  • 减少轮询提高效率:在没有条件变量的情况下,线程可能会反复轮询某个条件是否成立,这不仅浪费了 CPU 资源,还可能导致程序性能下降。条件变量的出现使得线程可以在不满足条件时进行休眠,将资源让给有需要的其他线程,从而提高了系统的整体效率。
  • 避免死锁和竞态条件:条件变量通常与互斥锁(Mutex)结合使用,以避免死锁和竞态条件。通过合理地管理锁和条件变量的使用,可以确保线程在访问共享资源时不会相互干扰,从而保证了程序的正确性和稳定性。
  • 实现复杂同步条件:有时候一个线程不仅需要知道资源可达,还需要知道一些额外信息。条件变量能够处理这种复杂的同步条件,使得线程在满足特定条件时才进行操作。例如,在生产者消费者问题中,条件变量可以用于判断缓冲队列是否已满或为空,从而控制生产者和消费者的执行顺序。

在C++中,条件变量类(std::condition_variable)提供了丰富的成员函数来支持上述功能。例如,wait() 函数允许线程等待某个条件成立,而 notify_one() 和 notify_all() 函数则用于唤醒正在等待的线程。这些函数与互斥锁结合使用,可以实现精细化的线程同步和协调。

1.2 std::condition_variable 的定义与功能

std::condition_variable 是 C++11 引入的一个重要组件,它主要用于多线程编程中的线程同步。在多线程环境中,不同的线程可能需要访问和修改共享资源,这就可能引发数据竞争和不一致性的问题。std::condition_variable 提供了一种机制,允许线程在特定条件不满足时阻塞,并在条件满足时被唤醒,从而实现了线程间的协调与同步。

定义

std::condition_variable 定义在头文件 <condition_variable> 中。它是一个模板类,用于阻塞一个或多个线程,直到某个线程修改了共享变量并通知 condition_variable。这种机制通常与互斥锁(std::mutex)一起使用,以避免死锁和其他同步问题。

功能

  • 线程同步:std::condition_variable 的核心功能是进行线程同步。它允许线程在特定条件不满足时进入阻塞状态,等待其他线程修改条件并发出通知。一旦条件满足,被阻塞的线程将被唤醒并继续执行。
  • 条件等待:线程可以通过调用 std::condition_variable 的 wait 函数来等待特定条件的满足。在调用 wait 时,线程会先解锁互斥锁,然后进入阻塞状态。当其他线程修改了条件并调用 notify_one 或 notify_all 时,被阻塞的线程会被唤醒并重新获取互斥锁。
  • 通知机制:当某个线程修改了共享变量并希望通知其他等待的线程时,可以调用 std::condition_variable 的 notify_one 或 notify_all 函数。notify_one 会唤醒一个等待的线程,而 notify_all 会唤醒所有等待的线程。
  • 避免假唤醒:在实际应用中,由于各种原因(如操作系统调度、中断等),线程可能会出现假唤醒现象,即在没有接收到通知的情况下自行唤醒。为了避免这种情况,std::condition_variable 的 wait 函数通常与循环一起使用,以确保线程在真正满足条件时才继续执行。

使用示例

下面是一个简单的示例,展示了如何使用 std::condition_variable 实现线程同步:

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
  
std::mutex mtx;  
std::condition_variable cv;  
bool ready = false;  
  
void print_id(int id) {  
    std::unique_lock<std::mutex> lck(mtx);  
    while (!ready) {  // 如果条件不满足,则等待  
        cv.wait(lck); // 当前线程被阻塞,当收到通知且条件满足时继续执行  
    }  
    // ... 执行其他操作 ...  
    std::cout << "thread " << id << '\n';  
}  
  
void go() {  
    std::unique_lock<std::mutex> lck(mtx);  
    ready = true;  // 修改条件变量  
    cv.notify_all();  // 通知所有等待的线程  
}  
  
int main() 
{  
    std::thread threads[10];  
    // spawn 10 threads:  
    for (int i = 0; i < 10; ++i)  
        threads[i] = std::thread(print_id, i);  
  
    std::cout << "10 threads ready to race...\n";  
    go();                       // go!  
  
    for (auto& th : threads) th.join();  
  
    return 0;  
}

上面代码的输出为:

10 threads ready to race...
thread 6
thread 7
thread 5
thread 4
thread 8
thread 3
thread 2
thread 0
thread 1
thread 9

这个示例创建了 10 个线程,它们都试图打印自己的 ID。但是,在 go() 函数被调用之前,这些线程都被阻塞在 cv.wait(lck) 处。当 go() 函数被调用并修改 ready 变量为 true 时,所有等待的线程都会被唤醒并继续执行。

2 std::condition_variable 的主要成员函数

2.1 wait() 函数

wait() 函数允许线程在满足特定条件之前进入阻塞状态。其主要功能是让当前线程进入阻塞状态,直到另一个线程调用 notify_one() 或 notify_all() 来唤醒它。在等待期间,当前线程会释放它持有的互斥锁(std::mutex),允许其他线程访问共享资源。一旦线程被唤醒,它会自动重新获取互斥锁,并继续执行后续的代码。

wait() 函数通常与 std::unique_lock 或 std::lock_guard 一起使用,以确保在调用 wait() 时正确地管理互斥锁。在上面的 “1.2 std::condition_variable 的定义与功能” 已经给出了 wait() 函数的简单使用样例。值得注意的是,为了避免假唤醒(spurious wakeup),上面例子中的 wait() 函数与循环一起使用,以确保在真正满足条件时才继续执行。假唤醒是指线程在没有收到通知的情况下自行唤醒的现象,这可能是由于操作系统调度或其他原因导致的。因此,通过循环检查条件变量,可以确保线程在真正满足条件时才退出等待状态。

wait() 函数也经常与 lambda 表达式结合使用,以提供更加灵活的条件检查方式。这样做的好处是可以在不离开 wait() 调用的情况下执行额外的逻辑,或者捕获并操作局部变量。

当使用 lambda 表达式时,wait() 函数会接受一个可调用对象(即 lambda),该对象会在每次唤醒后(但在重新获取锁之前)被调用,并返回一个布尔值。如果返回 true,则 wait() 退出并继续执行;如果返回 false,则线程再次进入阻塞状态。

下面是一个使用 lambda 表达式的 wait() 函数的示例:

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
#include <vector>  

std::mutex mtx;
std::condition_variable cv;
std::vector<int> data;
bool ready = false;

void worker_thread() {
	std::unique_lock<std::mutex> lck(mtx);
	cv.wait(lck, [] { return ready && !data.empty(); }); // 使用 lambda 表达式作为条件  

	// 当 ready 为 true 且 data 不为空时,继续执行  
	std::cout << "Processing data: " << data.front() << std::endl;
	data.erase(data.begin());
}

void add_data(int new_data) {
	std::lock_guard<std::mutex> lck(mtx);
	data.push_back(new_data);
	ready = true;
	cv.notify_one(); // 唤醒一个等待的线程  
}

int main()
{
	std::thread t1(worker_thread);
	std::thread t2(worker_thread);

	add_data(12);
	add_data(188);

	t1.join();
	t2.join();

	return 0;
}

上面代码的输出为:

Processing data: 12
Processing data: 188

在这个例子中,worker_thread 函数中的 cv.wait(lck, [] { return ready && !data.empty(); }); 使用了 lambda 表达式作为条件。这个 lambda 表达式检查 ready 是否为 true 以及 data 向量是否非空。只有当这两个条件都满足时,lambda 才会返回 true,wait() 函数才会退出,线程才会继续执行后续的代码。

注意,lambda 表达式在每次线程被唤醒时都会被调用,因此它应该是一个无副作用的操作,或者至少应该确保在多次调用中保持一致的行为。

使用 lambda 表达式增加了 wait() 函数的灵活性,使得能够在等待条件成立的同时执行额外的逻辑,或者根据当前状态调整等待条件。然而,这也增加了代码的复杂性,因此在使用时应确保代码清晰易懂,并仔细处理并发访问共享资源的问题。

2.2 wait_for() 函数

与 wait() 函数不同,wait_for() 允许线程等待指定的时间段,如果在这个时间段内条件没有满足,线程会主动放弃等待并继续执行。这提供了一种带超时的等待机制,使得线程在等待共享资源或某个条件成立时不会无限期地阻塞。

函数原型

wait_for() 函数的基本原型如下:

template< class Rep, class Period >  
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,  
                          const std::chrono::duration<Rep,Period>& timeout_duration );

其中:

  • lock 是一个对 std::unique_lock 或 std::lock_guard 的引用,它持有一个互斥锁,用于保护共享资源。
  • timeout_duration 是一个表示等待时间段的时长对象,由 std::chrono 库提供。

返回值

wait_for() 函数返回一个 std::cv_status 枚举值,用于指示等待的结果:

  • std::cv_status::timeout:表示等待超时,即在指定的 timeout_duration 时间段内条件没有满足。
  • std::cv_status::no_timeout:表示等待没有超时,即条件在超时前已经被满足。

使用方式

使用 wait_for() 时,通常将其放在一个循环中,以便在超时时重新检查条件,或者执行其他逻辑。下面是一个使用 wait_for() 的示例:

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
#include <chrono>  
  
std::mutex mtx;  
std::condition_variable cv;  
bool ready = false;  
  
void print_id(int id) {  
    std::unique_lock<std::mutex> lck(mtx);  
    auto start = std::chrono::high_resolution_clock::now();  
      
    while (!ready) {  
        // 等待最多5秒  
        if (cv.wait_for(lck, std::chrono::seconds(5)) == std::cv_status::timeout) {  
            std::cout << "Thread " << id << " timed out.\n";  
            return;  
        }  
    }  
      
    // 计算并打印等待时间  
    auto end = std::chrono::high_resolution_clock::now();  
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();  
    std::cout << "Thread " << id << " ready after " << duration << " milliseconds.\n";  
}  
  
void go() {  
    std::unique_lock<std::mutex> lck(mtx);  
    std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟一些工作  
    ready = true;  
    cv.notify_all(); // 唤醒所有等待的线程  
}  
  
int main() 
{  
    std::thread threads[10];  
    for (int i = 0; i < 10; ++i) {  
        threads[i] = std::thread(print_id, i);  
    }  
      
    go(); // 开始准备过程  
      
    for (auto& th : threads) {  
        th.join();  
    }  
      
    return 0;  
}

上面代码的输出为:

Thread 7 ready after 3013 milliseconds.
Thread 8 ready after 3013 milliseconds.
Thread 6 ready after 3021 milliseconds.
Thread 5 ready after 3024 milliseconds.
Thread 4 ready after 3030 milliseconds.
Thread 3 ready after 3038 milliseconds.
Thread 2 ready after 3042 milliseconds.
Thread 1 ready after 3045 milliseconds.
Thread 0 ready after 3050 milliseconds.
Thread 9 ready after 0 milliseconds.

在上面的例子中,每个线程都会等待 ready 变量变为 true,但是它们最多只等待5秒。如果5秒内 ready 没有变为 true,则 wait_for() 函数会返回 std::cv_status::timeout,线程会打印一条超时信息并退出循环。如果 ready 在5秒内变为 true,则线程会继续执行后续的代码。

注意事项

  • 超时处理:使用 wait_for() 时,必须考虑超时后的处理逻辑。超时并不意味着条件永远无法满足,可能只是暂时的延迟或竞争条件导致的。因此,在超时后,线程可以选择重试、记录错误、退出或其他适当的操作。
  • 互斥锁管理:与 wait() 函数一样,wait_for() 函数在调用期间会释放互斥锁,允许其他线程访问共享资源。当 wait_for() 返回时,互斥锁会被重新获取,因此不需要在调用前后手动加锁和解锁。
  • 避免忙等待:尽管 wait_for() 提供了带超时的等待机制,但如果错误地频繁调用 wait_for() 而没有合适的休眠或条件检查,可能会导致忙等待(busy-waiting),这会浪费处理器资源。因此,在使用 wait_for() 时,应该确保有一个合理的等待逻辑,并考虑使用更长的超时时间或在等待之间加入休眠。
  • 线程通知:当条件满足时,通常需要使用 notify_one() 或 notify_all() 来唤醒等待的线程。需要注意的是,如果没有线程在等待,或者等待的线程已经超时,这些通知操作将不会有任何效果。
  • 条件变量的使用场景:wait_for() 通常在需要等待某个条件成立或超时的情况下使用。例如,在生产者-消费者模型中,消费者线程可以等待队列中有数据可用,或者在等待一段时间后放弃等待以避免无限期的阻塞。

wait_for() 函数也可以与 lambda 表达式结合使用,以便在等待条件时执行更复杂的逻辑或检查。Lambda 表达式可以在 wait_for() 的循环内部使用,以定义等待期间需要重复执行的检查或操作。

下面是一个示例,演示了如何使用 wait_for() 和 lambda 表达式来等待一个条件成立,并在等待期间执行一些操作:

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
#include <chrono>  

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
	std::unique_lock<std::mutex> lck(mtx);
	auto predicate = [&] {
		// 这里可以执行任何需要在等待期间重复进行的操作  
		std::cout << "Worker thread is checking the condition...\n";
		return ready; // 返回条件是否满足  
	};

	if (!cv.wait_for(lck, std::chrono::seconds(5), predicate)) {
		std::cout << "Worker thread timed out!\n";
	}
	else {
		std::cout << "Worker thread got the condition!\n";
	}
}

void set_ready() {
	std::unique_lock<std::mutex> lck(mtx);
	std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟一些工作  
	ready = true;
	cv.notify_all(); // 唤醒所有等待的线程  
}

int main() {
	std::thread worker_thread(worker);
	std::thread setter_thread(set_ready);

	worker_thread.join();
	setter_thread.join();

	return 0;
}

上面代码的输出为:

Worker thread is checking the condition...
Worker thread is checking the condition...
Worker thread got the condition!

在这个示例中,worker() 函数定义了一个 lambda 表达式 predicate,它会在每次 wait_for() 超时或被唤醒时执行。Lambda 表达式检查 ready 变量是否变为 true,并且打印一条消息到控制台。如果 ready 在5秒内变为 true,则 wait_for() 返回 std::cv_status::no_timeout,线程会继续执行并打印成功的消息。如果超时,则 wait_for() 返回 std::cv_status::timeout,线程会打印超时的消息。

set_ready() 函数模拟了某些工作,并在完成后设置 ready 为 true 并通知所有等待的线程。

通过这种方式,开发者可以使用 lambda 表达式来定义更复杂的条件检查或等待期间的行为,而不仅仅是简单地等待一个布尔变量变为 true。Lambda 表达式为 wait_for() 提供了更大的灵活性,使开发者可以编写更加健壮和可维护的并发代码。

2.3 wait_until() 函数

wait_until() 函数允许线程等待直到指定的时间点到达,或者直到某个条件成立。这个函数非常有用,尤其是在需要线程在某个特定时间之前等待某个条件成立时。

函数原型

wait_until() 函数的基本原型如下:

template< class Clock, class Duration >  
cv_status wait_until( std::unique_lock<std::mutex>& lock,  
                      const std::chrono::time_point<Clock,Duration>& timeout_time,  
                      Predicate pred );

其中:

  • lock 是一个 std::unique_lockstd::mutex 对象,它必须锁定一个互斥量(mutex)。在调用 wait_until() 时,这个互斥量会被自动释放,允许其他线程获取锁。当 wait_until() 返回时,互斥量会再次被锁定。
  • timeout_time 是一个 std::chrono::time_point 对象,指定了线程应该等待到的绝对时间点。
  • pred 是一个谓词(通常是一个函数或 lambda 表达式),它应该返回一个布尔值,表示线程应该继续等待的条件。

返回值

wait_until() 函数返回一个 std::cv_status 枚举值,它可以是以下之一:

  • std::cv_status::no_timeout:如果谓词 pred 在 timeout_time 之前返回 true,则线程不会超时,并继续执行。
  • std::cv_status::timeout:如果达到了 timeout_time 时间点而谓词 pred 仍未返回 true,则线程超时。

使用场景

wait_until() 在许多并发编程场景中都非常有用,特别是当线程需要等待某个特定的时间窗口,或者等待某个事件在特定时间之前发生时。例如,有这样一个线程,它需要在每天的特定时间执行某些操作,或者需要在另一个线程完成某个任务后的某个时间段内执行。

示例代码

下面是一个简单的示例,展示了如何使用 wait_until() 函数:

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
#include <chrono>  
#include <atomic>  

std::mutex mtx;
std::condition_variable cv;
std::atomic<bool> ready(false);

void worker() {
	std::unique_lock<std::mutex> lck(mtx);

	// 计算超时时间,这里设置为当前时间加上5秒  
	auto timeout = std::chrono::system_clock::now() + std::chrono::seconds(5);

	// 使用lambda表达式作为谓词  
	auto predicate = [&] {
		std::cout << "Worker thread checking if ready...\n";
		return ready.load(); // 检查ready是否为true  
	};

	if (cv.wait_until(lck, timeout, predicate)) {
		std::cout << "Worker thread got the condition!\n";
	}
	else {
		std::cout << "Worker thread timed out!\n";
	}
}

void set_ready() {
	std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟一些工作  
	std::cout << "Setting ready to true...\n";
	ready.store(true);
	cv.notify_all(); // 唤醒所有等待的线程  
}

int main() {
	std::thread worker_thread(worker);
	std::thread setter_thread(set_ready);

	worker_thread.join();
	setter_thread.join();

	return 0;
}

上面代码的输出为:

Worker thread checking if ready...
Setting ready to true...
Worker thread checking if ready...
Worker thread got the condition!

在这个例子中,worker() 函数中的线程会等待 ready 变量变为 true,或者直到一个特定的超时时间(当前时间加上5秒)。它使用了一个 lambda 表达式作为谓词来检查 ready 的状态。如果 ready 在超时之前变为 true,线程将继续执行;否则,它将超时并打印一条消息。

set_ready() 函数模拟了一些工作,并在完成后设置 ready 为 true 并通知所有等待的线程。

这个示例展示了 wait_until() 的基本用法(同时也使用了一个 lambda 表达式 predicate,它会在 wait_until() 超时或被唤醒时执行),它允许线程在等待某个条件成立时有一个明确的超时机制。这有助于防止线程无限期地等待,提高了程序的响应性和可靠性。

2.4 唤醒机制

std::condition_variable 提供了两种通知机制来唤醒等待的线程:notify_one() 和 notify_all()。

(1)notify_one()

notify_one() 函数用于唤醒等待在 std::condition_variable 对象上的一个线程。如果有多个线程正在等待该条件变量,则只有一个线程会被唤醒并继续执行。这个函数不保证唤醒哪个线程,选择是随机的。

函数原型:

void notify_one() noexcept;

使用场景:

当只需要唤醒一个线程来处理某个事件或条件时。
当多个线程等待相同的条件,但只需要一个线程来处理时。

示例:

std::mutex mtx;  
std::condition_variable cv;  
bool ready = false;  
  
void worker() {  
    std::unique_lock<std::mutex> lck(mtx);  
    while (!ready) { // 等待条件成立  
        cv.wait(lck); // 释放锁并等待,直到被唤醒或虚假唤醒  
    }  
    // 执行任务...  
}  
  
void set_ready() {  
    {  
        std::lock_guard<std::mutex> lck(mtx);  
        ready = true;  
    }  
    cv.notify_one(); // 唤醒一个等待的线程  
}

在这个例子中,当 set_ready() 函数被调用时,它会设置 ready 为 true,并使用 notify_one() 唤醒一个正在等待的线程。注意,ready 变量的修改和 notify_one() 的调用都被包含在互斥量的锁定范围内,以确保线程安全。

(2)notify_all()

notify_all() 函数用于唤醒等待在 std::condition_variable 对象上的所有线程。当这个函数被调用时,所有正在等待的线程都会被唤醒。

函数原型:

void notify_all() noexcept;

使用场景:

当所有等待的线程都需要被唤醒以处理某个事件或条件时。
当多个线程可以并行处理相同的任务时。

示例:

std::mutex mtx;  
std::condition_variable cv;  
std::queue<int> data_queue;  
  
void worker() {  
    while (true) {  
        std::unique_lock<std::mutex> lck(mtx);  
        cv.wait(lck, []{ return !data_queue.empty(); }); // 等待队列不为空  
        int item = data_queue.front();  
        data_queue.pop();  
        lck.unlock();  
        // 处理 item...  
    }  
}  
  
void add_data(int new_data) {  
    {  
        std::lock_guard<std::mutex> lck(mtx);  
        data_queue.push(new_data);  
    }  
    cv.notify_all(); // 唤醒所有等待的线程  
}

在这个例子中,add_data() 函数会在向队列中添加新数据时调用 notify_all(),从而唤醒所有正在等待队列不为空的线程。这些线程会并发地从队列中取出数据并处理它。

(3)注意事项

  • 在调用 notify_one() 或 notify_all() 之前,必须确保条件变量相关的状态已经更新,并且这个更新是在互斥量的保护下完成的。否则,可能会导致通知过早发出,或者等待的线程看到不一致的状态。
  • 被唤醒的线程需要重新检查条件,因为虚假唤醒(spurious wakeup)是可能的。即使没有 notify_one() 或 notify_all() 的调用,等待的线程也可能被唤醒。因此,通常建议使用循环和谓词来等待条件成立。
  • 在等待条件变量时,应始终使用 std::unique_lock 或其他锁定机制来确保互斥量的正确管理。std::condition_variable::wait() 和 std::condition_variable::wait_for() 等函数会自动释放锁,并在返回时重新获取锁,这有助于避免死锁和其他同步问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值