• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-03-12 17:58 Aet 隐藏边栏 |   抢沙发  3 
文章评分 1 次,平均分 5.0

thread相关

创建线程

  1. 使用 std::thread 构造函数创建线程,需传递一个可调用对象(函数、Lambda、函数对象等)

可调用对象

  1. 普通函数(函数指针)

  1. 函数对象(仿函数,Functor
    1. 定义一个重载了 operator() 的类:

  1. lambda表达式
    1. 直接在线程构造函数中定义行为:

  1. 成员函数(成员函数指针)
    1. 需要绑定对象实例和成员函数:

  1. std::function 包装的可调用对象

  1. std::bind 生成的绑定对象
    1. 适配参数或绑定对象实例:

join

  1. 阻塞当前线程(通常是主线程),直到被调用的线程执行完毕
    1. 确保线程执行完成后才继续后续操作

  1. 为什么必须调用 join()detach()
    1. std::thread 对象销毁前,必须调用 join()detach(),否则程序会调用 std::terminate 终止
    2. 设计意图:强制开发者明确管理线程的生命周期,避免悬空线程或资源泄漏

  1. join() 的阻塞行为
    1. 调用 join() 的线程会阻塞,直到目标线程结束
    2. 适用场景:需要等待子线程完成后再继续主线程逻辑(例如计算结果依赖子线程的输出)

  1. 注意:确保被 join() 的线程对象仍然有效

  1. 注意:一个线程对象只能被 join() 一次
    1. 调用后线程对象不再关联任何线程

  1. 注意:如果线程函数抛出异常,需确保 join() 仍会被调用:

detach

  1. 将线程与 std::thread 对象分离,使其在后台独立运行
    1. 调用 detach() 后,主线程不再管理子线程的生命周期,子线程独立运行
    2. 主线程继续执行,不会等待子线程完成
    3. 子线程结束后,其资源由 C++ 运行时库自动回收

  1. 为什么必须调用 join()detach()
    1. std::thread 对象销毁前,必须调用 join()detach(),否则程序会调用 std::terminate 终止
    2. 设计意图:强制开发者明确管理线程的生命周期,避免悬空线程或资源泄漏
  2. detach使用场景
    1. 后台任务:例如日志记录、心跳检测、网络监听等不需要同步的任务
    2. 不关心结果:当子线程的执行结果不影响主线程逻辑时

  1. 注意:分离后,子线程可能访问已销毁的对象,需确保数据有效性:

  1. 注意:一个线程对象只能调用一次 detach()

  1. 注意:分离后无法通过 std::thread 对象获取线程状态或结果:

joinable

  1. 作用:
    1. 检查线程状态:判断线程对象是否关联一个活跃的线程(即线程已启动但未调用 join()detach()
    2. 安全操作:避免对无效线程调用 join()detach(),防止程序崩溃
  2. 函数原型:
    1. 返回 true:线程对象关联一个活跃线程,可以调用 join()detach()
    2. 返回 false:线程对象未关联线程,或线程已被 join()/detach()

  1. 何时返回true(满足以下条件时,joinable() 返回 true:)
    1. 线程已通过构造函数启动(例如 std::thread t(func)
    2. 尚未对该线程调用 join()detach()
    3. 线程对象未被移动(例如 std::thread t2 = std::move(t1),移动后 t1 变为未关联)

  1. 注意:
    1. 调用 join()detach() 前,建议检查线程是否可操作

joindetach对比

  1. 概述
特性 join() detach()
线程所有权 主线程等待子线程完成 子线程独立运行,与主线程分离
资源释放 自动释放资源 系统自动释放资源(但需自行管理生命周期)
典型场景 需要等待结果 后台任务(如日志、网络心跳)
  1. 最佳实践
    1. 优先使用 join()
      在需要同步或处理子线程结果时,优先使用 join(),避免数据竞争和生命周期问题
    2. 谨慎使用 detach()
      仅在以下情况下使用 detach()
      1 子线程完全独立,无需与主线程交互
      2 确保子线程不会访问无效数据(如局部变量)
    3. 使用 RAII 管理线程
      通过封装类或 std::jthreadC++20)自动管理线程生命周期

线程ID

  1. 通过 std::this_thread::get_id() 获取当前执行线程的 ID

  1. 线程对象的 ID
    1. 使用 t.get_id() 获取线程唯一标识

  1. 线程 ID 的平台相关性
    1. std::thread::idC++ 标准库的逻辑 ID,与操作系统线程 ID(如 Linuxpthread_tWindowsDWORD)无关
    2. 若需获取底层线程 ID,可通过 std::thread::native_handle(),但会牺牲可移植性

  1. 注意:
    1. C++ 标准库中,通过 std::this_thread::get_id()std::thread::get_id() 获取的线程 ID(类型为 std::thread::id)并不是操作系统底层的实际线程 ID
    2. 返回平台相关的线程句柄,可进一步转换为原生线程 ID

线程移动语意

  1. std::thread 不可复制,但可通过移动转移所有权:

竞态条件

  1. 多个线程访问共享数据时需使用同步机制(如 std::mutex

  1. 死锁
    1. 避免在多个锁之间形成循环等待
    2. 按固定顺序加锁,或使用 std::lock 同时加锁:

传递参数

  1. 默认行为
    1. 参数按值拷贝到线程的独立内存空间,但需注意参数的生存期
    2. 向线程传递参数时,参数会被拷贝或移动,确保生命周期

  1. 传递引用
    1. 若需传递引用,使用 std::ref(但需确保引用的对象生命周期足够长):

异常安全

  1. 若线程函数抛出异常且未被捕获,程序会调用 std::terminate()
    1. 正确做法:在线程函数内部用 try/catch 捕获所有异常

  1. 如果主线程可能抛出异常,需确保异常前已处理子线程:

线程池

  1. 线程的创建和销毁成本较高,频繁创建线程可能导致性能下降
    1. 推荐使用线程池(如第三方库或 C++23std::execution
  2. 线程数量不应超过硬件支持的并发线程数(通过 std::thread::hardware_concurrency() 获取)

jthread相关

  1. 通过包装类(如 C++20std::jthread)自动管理线程生命周期,避免遗漏
    1. 析构时自动调用 join(),支持协作式取消(通过 request_stop()

  1. std::stop_token
    1. 用于优雅地终止线程

多线程同步相关

mutex

mutex -自动锁管理(RAII

  1. std::lock_guard
    1. 自动在作用域内加锁/解锁,不可手动控制

  1. std::unique_lock
    1. 更灵活的锁(可延迟加锁、手动控制)

  1. 其他mutex类型
类型 特性
std::recursive_mutex 允许同一线程多次加锁(解决递归函数中的锁重入问题)
std::timed_mutex 支持超时加锁(try_lock_for, try_lock_until
std::shared_mutex (C++17) 读写锁:写独占,读共享(lock_shared() 为读锁,lock() 为写锁)

condition_variable 条件变量

  1. 用于线程间通知,避免忙等待(Busy Waiting
  2. 基本用法

  1. 注意事项
    1. 虚假唤醒(Spurious Wakeup):wait 需在条件检查循环中使用:
    2. 批量通知:cv.notify_all() 唤醒所有等待线程

原子操作

  1. 无需锁的轻量级线程安全操作,适用于简单数据类型
  2. std::atomic 类型

  1. 内存顺序
    1. 指定原子操作的内存同步语义(如 memory_order_relaxed, memory_order_seq_cst):

死锁

  1. 不死锁条件

    1. 互斥资源竞争
    2. 持有并等待
    3. 不可剥夺
    4. 循环等待
  2. 死锁避免方法:

    1. 固定加锁顺序:所有线程按相同顺序获取锁
    2. 使用 std::lock 批量加锁:

线程安全的数据结构设计

  1. 可以通过封装共享数据
    1. 将共享数据与互斥量封装在同一个类中
    2. 通过接口控制访问

注意事项

  1. 减少锁的粒度
    1. 使用细粒度锁(如每个资源一个锁)
    2. 避免在临界区执行耗时操作(如 I/O
  2. 无锁编程(Lock-Free
    1. 适用场景:高并发简单操作(如计数器)
    2. 使用原子操作或 std::atomic 实现
  3. 线程局部存储(TLS
    1. 使用 thread_local 关键字避免共享:

多线程同步其他

std::lock_guard

  1. 特性
    1. 仅提供作用域内的自动加锁/解锁
    2. 构造时立即加锁,析构时自动解锁
    3. 无法转移所有权
  2. 场景
    1. 简单的临界区保护(无需延迟加锁或提前解锁)
    2. 需要轻量级锁管理的场景
  3. 优点与限制
优点 限制
代码简洁,无手动管理风险 无法手动控制加锁/解锁时机
性能开销极小 不支持条件变量
无法与其他锁配合批量加锁
  1. 示例

std::unique_lock

  1. 特性
    1. 支持手动加锁/解锁、延迟加锁、超时加锁
    2. 通过移动语义转移锁的所有权
    3. 可与 std::condition_variable 配合使用
  2. 场景
    1. 需要延迟加锁或提前解锁
    2. 配合条件变量实现线程间通信
    3. 批量加锁多个互斥量(避免死锁)
  3. 构造函数模式
模式 说明
std::defer_lock 延迟加锁(需手动调用 lock()
std::try_to_lock 尝试加锁(非阻塞)
std::adopt_lock 假定已持有锁(直接接管锁的所有权)
  1. 优点与限制
优点 限制
支持灵活加锁/解锁 性能略高于 std::lock_guard
可配合条件变量使用 代码复杂度稍高
支持批量加锁(std::lock
  1. 示例代码

  1. 关键对比
特性 std::lock_guard std::unique_lock
加锁时机 构造时立即加锁 可延迟加锁(通过 std::defer_lock
手动解锁 不支持 支持 (unlock())
条件变量支持 不支持 支持
性能开销 极低(接近原生互斥量) 稍高(因需维护额外状态)
移动语义 不可移动 支持所有权转移
适用场景 简单临界区保护 复杂锁管理或条件变量
  1. 场景一:条件变量(std::unique_lock 必需

  1. 场景二:批量加锁避免死锁

  1. 场景三:延迟加锁优化性能

  1. 使用建议
    1. 在简单场景下优先使用 std::lock_guard
    2. 在需要灵活性时,接受 std::unique_lock 的微小性能损失

线程池相关

  1. 示例代码

一次性事件

async

  1. 作用
    1. 简化异步任务:自动启动线程或任务,返回 std::future
  2. 启动策略
    1. std::launch::async:强制新线程执行
    2. std::launch::deferred:延迟到 get()/wait() 时在当前线程执行

  1. 场景
    1. 简单的异步计算(如文件加载、数学运算)
    2. 需要快速获取结果的轻量级任务
  2. 不加参数std::launch::async,是不是不是新线程?
    1. 默认情况下,std::async 的启动策略是 组合模式

  1. 可能的情况一:
    1. 启动新线程(类似 std::launch::async
    2. 大多数现代标准库(如 MSVCGCCClang)默认会选择 async,立即创建新线程执行任务

  1. 可能的情况二:
    1. 延迟执行(类似 std::launch::deferred
    2. 少数实现可能在某些情况下选择 deferred(例如资源紧张时)

  1. 若需确保任务始终异步执行,应显式指定 std::launch::async

future

  1. 作用
    1. 占位符:表示一个未来可能获取的值(或异常)
    2. 阻塞等待:通过 get() 方法阻塞当前线程,直到结果就绪
    3. 一次性读取:只能调用一次 get()

  1. wait
    1. 阻塞当前线程,直到关联的异步任务完成(无论任务是否返回结果)
    2. 仅等待任务完成,不返回任何值或异常
    3. 可多次调用
      第一次调用的时候,如果任务已经完成,不会阻塞。如果任务还没完成,会阻塞。
      第二次调用的时候,如果任务已经完成,不会阻塞。如果任务还没完成,继续阻塞。
    4. 场景
      需要等待异步任务完成后再继续后续逻辑,但不需要任务的结果
      检查任务是否已完成(结合 wait_for()wait_until()
  2. get
    1. 阻塞当前线程,直到关联的异步任务完成,并返回任务的结果(或抛出异常)
    2. 调用后,std::future 对象变为无效(valid() == false
    3. 不可重复调用
    4. 场景
      需要获取异步任务的返回值或处理任务中抛出的异常
      确保任务完成后才使用其结果

  1. 组合使用场景:先 wait()get()

  1. 组合使用场景:避免多次调用 get()
    1. get() 只能调用一次,多次调用会导致未定义行为:

  1. std::shared_future
    1. 若需要多次获取结果,可将 std::future 转换为 std::shared_future

package_task

  1. 作用
    1. 包装任务:将可调用对象(函数、Lambda)与 std::future 绑定
    2. 灵活调度:可将任务传递给线程池或其他线程执行
  2. 场景
    1. 需要手动控制任务调度(如线程池)
    2. 需要将任务存储到队列中延迟执行

promise

  1. 作用
    1. 显式传值:通过 set_value()set_exception() 手动设置结果
    2. future 关联:通过 get_future() 获取关联的 std::future
  2. 场景
    1. 需要在线程间手动传递结果(如回调函数)
    2. 需要将异步结果与复杂逻辑解耦

协作示例

  1. 使用线程池执行任务,并通过 std::packaged_taskstd::future 获取结果

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享