同步

等待事件

持续等待

当一个线程等待另一个线程完成任务时,持续的检查共享数据标志(用于做保护工作的互斥量),直到另一线程完成工作时对这个标志进行重设。但这样一方面等待线程会一直被阻塞,浪费了资源;而被等待线程如果被打断或者等待还会增加等待线程的等待时间。

周期间歇

当被等待线程要做其他事时,可以暂时释放互斥量:

bool flag;
std::mutex m;

void wait_for_flag()
{
  std::unique_lock<std::mutex> lk(m);
  while(!flag)
  {
    lk.unlock();  // 1 解锁互斥量
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 2 休眠100ms
    lk.lock();   // 3 再锁互斥量
  }
}

但是很难确定正确的休眠时间,并且要小心互斥量的变化。

条件变量

https://cppreference.cn/w/cpp/thread/condition_variable;
https://cppreference.cn/w/cpp/thread/condition_variable_any
这是优先选择,由另一线程来进行唤醒。C++标准库对条件变量有两套实现:std::condition_variablestd::condition_variable_any。这两个实现都包含在 <condition_variable> 头文件的声明中。两者都需要与一个互斥量一起才能工作;前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作。 std::condition_variable_any 更加通用,不过考虑到开销,std::condition_variable 一般作为首选的类型,当对灵活性有硬性要求时,我们才会去考虑 std::condition_variable_any
std::condition_variable 为例,提供了两种类型的功能:等待和唤醒。
由于唤醒操作的不确定性,唤醒的线程有可能无法工作,因此推荐使用带条件谓词的 wait
wait(lock, predicate)
执行逻辑如下:

  • 持有锁检查谓词: wait 函数首先检查提供的 predicate (通常是一个 lambda 表达式)。此时传入的 lock 必须是锁定的状态。
  • 如果谓词为 true: 说明条件已经满足,wait 函数直接返回。线程继续执行,lock 仍然保持锁定状态。
  • 如果谓词为 false: 说明条件不满足,线程需要等待。此时 wait 执行一个原子操作序列:
    a. 释放锁: wait原子调用 lock.unlock(),释放掉传入的 lock 所管理的互斥锁。这样就允许其他线程能够获取这个锁;
    b. 阻塞线程: 当前线程进入阻塞状态,等待被 notify_one()notify_all() 唤醒。
  • 重新获取锁: 在唤醒后,wait 函数原子地尝试重新调用 lock.lock() 获取之前释放的互斥锁。直到成功获取锁为止。
  • 再次检查谓词: 成功重新获取锁后,wait再次检查 predicate。

可能的死锁

根据 wait的流程,一般与 std::unique_lock <std::mutex> 配合使用,而不是 std::lock_guard<std::mutex>。 由于不能提前解锁,有可能导致死锁。

期望事件

“期望”(future)是c++标准库提供的一种同步机制,用于表示一个一次性异步操作的未来结果。相比起条件变量,它更强调线程之间数据的传递,而不需要复杂的同步机制。

期望 条件变量
目的 线程间一次性传递值 线程间等待/通知机制
状态 事件发生后状态固定,不能重置 可多次触发
数据传递 直接传递 通过共享变量传递
使用场景 一次性事件,如任务结果 重复事件,如队列数据就绪

C++ 中通过两种模板类型实现:
std::future:唯一期望,一个实例仅关联一个事件,不可复制https://cppreference.cn/w/cpp/thread/future
std::shared_future:共享期望,多个实例可关联同一事件,可复制,适用于多线程等待同一事件。
https://cppreference.cn/w/cpp/thread/shared_future
模板参数 T 为事件关联的数据类型,若无需传递数据,可使用 std::future<void>

使用期望

如上所述, std::future 对象在内部存储一个将来会被某个 Provider 赋值的值,并提供了一个访问该值的机制:通过 get() 成员函数实现。如果试图在 get() 函数可用之前通过它来访问相关的值,那么将会阻塞,直到该值可用。

创建期望

一个有效的 std::future 对象通常由以下三种 Provider 创建:通过三种 Providerstd::asyncstd::packaged_taskstd::promise。而提供的默认构造函数创建的 std::future 对象都是无效的。

std::async

std::async 是一个函数模板,用于启动异步任务并返回一个 std::future 对象,该对象用于保存异步操作的结果。实际上它封装了线程的创建和管理,使得异步编程更加简单和安全。

template< class F, class... Args >
std::future<result_of_T> async( std::launch policy(可选), F&& f, Args&&... args );
// policy:启动策略,指定任务执行的方式
// f:要异步执行的可调用对象(函数、Lambda 表达式、函数对象等)。
// Args:传递给可调用对象 f 的参数。
//传参方式同std::thread

提供三种启动策略:

  • std::launch::async:立即执行,传递的函数会在一个新线程中立即执行。
  • std::launch::deferred:延迟执行,延迟到返回的 future对象调用 get() 或者 wait() 执行,在调用者线程上执行。而如果不调用,就不会执行任务。
  • std::launch::async | std::launch::deferred:默认行为。由系统确定使用哪种启动策略。

std::promise

https://cppreference.cn/w/cpp/thread/promise/set_value
类模板 std::promise<T> 提供了一个机制,它允许显式地设置一个值或异常,然后通过与之关联的 std::future 对象在其他线程中获取这个结果。这样就提供了一种简单的线程传递数据方法
以一个生产者线程、消费者线程协作为例:

#include <iostream>
#include <thread>
#include <future>
#include <chrono>

//生产者线程:传递结果
void producer_task(std::promise<int> result_promise, int input) {
    //耗时计算
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    //将结果设置到 promise 中(传递到消费者线程)
    result_promise.set_value(square);
}

//消费者线程:等待结果并处理
void consumer_task(std::future<int> result_future) {
    //阻塞等待结果(从生产者线程获取)
    int result = result_future.get();
    
    //处理结果
    std::cout << "消费者线程收到结果: " << result << std::endl;
}

int main() {
    //创建 promise 和 future
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    //启动生产者线程
    std::thread producer(producer_task, std::move(prom), 5);

    //启动消费者线程
    std::thread consumer(consumer_task, std::move(fut));

    //等待线程结束
    producer.join();
    consumer.join();

    return 0;
}

注意事项:

  • 在传递 promise 对象和 future对象时,需要使用 std::move,因为都禁止了拷贝。可以使用 std::ref 进行引用,但是要注意生命周期。

std::packaged_task

https://cppreference.cn/w/cpp/thread/packaged_task

template< class R, class ...ArgTypes >
class packaged_task<R(ArgTypes...)>;

它将一个可调用对象(函数、Lambda、函数对象等)包装成一个异步任务,并与一个 std::future 对象关联,用于获取该任务的执行结果。使用方式类似 **std::promise*。

对比

特性 std::promise std::async std::packaged_task
抽象级别 低级(手动控制) 高级(自动管理) 中级(函数包装器)
易用性 复杂 简单 中等
控制粒度 高(完全控制) 低(自动管理) 中(控制执行时机)
线程管理 手动 自动 手动
适用场景 复杂同步、自定义逻辑 简单异步调用 任务队列、线程池
函数包装 无,需手动调用函数 自动包装函数 显式包装函数
结果传递 手动设置值/异常 自动传递返回值/异常 自动传递返回值/异常
执行时机 完全自定义 立即或延迟(取决于策略) 可控制执行时机
移动语义 必须使用std::move 自动处理 必须使用std::move
异常处理 手动设置异常 自动捕获并传递 自动捕获并传递
性能开销 中(可能创建线程)

待更新

这一节写的很乱,感觉主要是还是没有太多使用过这些工具。以后慢慢记录更新吧

posted @ 2025-09-05 21:48  名字好难想zzz  阅读(5)  评论(0)    收藏  举报