并发基础概念
并发基础概念
并发编程要解决的是:让多个任务在时间上重叠推进,同时仍能得到可预测、可维护的结果。它不等于“创建很多线程”;共享状态的所有权、同步方式和退出路径才是正确性的核心。
并发、并行与异步
| 概念 | 关注点 | 是否需要多核 | 例子 |
|---|---|---|---|
| 并发(concurrency) | 多个任务在同一时间段内交替推进 | 不需要 | 单核上交替处理网络连接与日志写入 |
| 并行(parallelism) | 多个任务在同一时刻真正执行 | 通常需要 | 多核同时计算不同数据块 |
| 异步(asynchrony) | 发起操作后先做别的事,结果稍后取得 | 不需要 | 提交任务后通过 std::future 取得结果 |
异步是程序组织方式,并发和并行是任务的执行现象。一个异步任务可能延迟到调用 get() 时才在当前线程执行;一个多线程程序也可能因为锁竞争而几乎没有并行收益。详见异步机制。
1 | 单核并发: AAAAA BBBBB AAAAA |
进程与线程
进程通常是操作系统进行隔离和资源管理的单位;线程是进程内的执行单元。具体实现随操作系统而异,下面是 POSIX/Linux 等常见模型。
| 资源或状态 | 同一进程的线程是否共享 |
|---|---|
| 虚拟地址空间、堆、全局/静态对象 | 共享 |
| 打开的文件描述符 | 通常共享同一描述表语义 |
| 每个线程的栈、寄存器上下文、程序计数器 | 不共享 |
thread_local 对象 |
每线程一份 |
线程共享内存使通信成本低,也意味着一个线程写入的数据会影响其他线程。跨进程通信则需要管道、Unix 域套接字、共享内存等 IPC;共享内存同样需要同步。
为什么使用并发
- I/O 密集型程序可在一个任务等待网络或磁盘时推进其他任务。
- 独立的 CPU 密集型计算可在多核上并行。
- 交互程序可将耗时工作移出响应线程。
但线程不是免费的:创建、调度、缓存失效、同步与调试都有成本。CPU 密集型任务的可运行线程通常不应远多于可用核心;I/O 任务也应设置队列、连接或任务数量上限。
共享状态、竞态条件与数据竞争
**竞态条件(race condition)**指结果依赖线程交错顺序。例如“先检查队列非空,再取元素”若不作为一个受保护的整体,就可能出错。
数据竞争(data race)是 C++ 内存模型中的严格术语:两个线程并发访问同一内存位置,至少一个是写入,且这些访问不是原子操作、也没有建立 happens-before 关系。数据竞争会导致未定义行为,不只是“偶尔计数不准”。
1 |
|
修复方式取决于不变量:单个计数器可使用 std::atomic<int>;需要让“检查、修改、多对象更新”保持整体一致时,使用互斥锁。详见同步机制。
happens-before:可见性与顺序的依据
“一个线程已经写过”不自动意味着另一个线程能按预期看到它。C++ 用 happens-before(先行发生) 描述跨线程可见性和顺序关系。常见建立方式包括:
- 同一
std::mutex的解锁与随后成功加锁; - 对同一原子对象的 release 写入与读到该值的 acquire 读取;
- 线程函数完成与另一个线程成功
join()返回; - 条件变量等待方被唤醒后重新获得用于保护条件的互斥锁。
原子性只保证一次原子读写不被拆开;它不自动维护多个变量之间的一致性。内存序和 CAS 的边界见CAS。
常见问题
| 问题 | 含义 | 常见处理方向 |
|---|---|---|
| 死锁 | 线程循环等待彼此持有的资源 | 固定加锁顺序,或使用 std::scoped_lock |
| 活锁 | 线程都在运行、反复让步,却没有推进工作 | 限制重试,加入退避,重新设计协议 |
| 饥饿 | 某任务长期得不到执行或资源 | 降低不公平竞争,限制队列与优先级 |
| 伪共享 | 不同线程频繁写同一缓存行上的不同变量 | 调整数据布局;先用性能分析确认 |
实践建议
- 优先减少共享可变状态:消息传递、不可变数据和明确所有权通常比“到处加锁”更易维护。
- 把临界区做小,但不要为了缩短锁范围而把本应原子完成的操作拆开。
- 先定义停止、异常和资源生命周期,再启动线程。
- 使用
-fsanitize=thread(ThreadSanitizer,平台支持时)排查数据竞争;它不能证明程序完全正确。



