C++ 多线程知识体系
一、多线程基础概念
1. 线程与进程
- 进程:操作系统资源分配的基本单位,拥有独立的地址空间
- 线程:CPU调度的基本单位,共享进程的资源,有独立的栈和寄存器
2. 并发与并行
- 并发:宏观上同时执行,微观上交替执行
- 并行:真正的同时执行,需要多核CPU支持
3. 线程安全
- 多个线程访问共享资源时不会出现数据不一致或不可预期的结果
- 实现方式:互斥锁、原子操作、线程局部存储等
二、C++多线程支持
1. C++11标准引入的线程库
#include <thread>
std::thread t(func, args...); // 创建线程
t.join(); // 等待线程结束
t.detach(); // 分离线程
2. 线程管理
- 线程ID:
std::thread::id,可通过std::this_thread::get_id()获取 - 硬件并发数:
std::thread::hardware_concurrency() - 线程休眠:
std::this_thread::sleep_for(),std::this_thread::sleep_until()
三、线程同步机制
1. 互斥量(Mutex)
#include <mutex>
std::mutex m;
m.lock();
// 临界区
m.unlock();
// 推荐使用RAII风格的锁
std::lock_guard<std::mutex> lock(m);
std::unique_lock<std::mutex> ulock(m, std::defer_lock);
2. 条件变量(Condition Variable)
#include <condition_variable>
std::condition_variable cv;
std::mutex m;
bool ready = false;
// 等待线程
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, []{return ready;});
// 通知线程
{
std::lock_guard<std::mutex> lock(m);
ready = true;
}
cv.notify_one(); // 或 notify_all()
3. 原子操作
#include <atomic>
std::atomic<int> counter(0);
counter.fetch_add(1); // 原子递增
4. 读写锁(C++17)
#include <shared_mutex>
std::shared_mutex sm;
// 写锁
{
std::unique_lock<std::shared_mutex> lock(sm);
// 写操作
}
// 读锁
{
std::shared_lock<std::shared_mutex> lock(sm);
// 读操作
}
四、高级线程管理
1. 线程池实现
class ThreadPool {
public:
ThreadPool(size_t);
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
~ThreadPool();
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};
2. 异步操作
#include <future>
// 异步执行
auto future = std::async(std::launch::async, func, args...);
auto result = future.get(); // 获取结果
// 承诺和未来值
std::promise<int> p;
auto f = p.get_future();
p.set_value(42); // 设置值
3. 线程局部存储
thread_local int thread_specific_value = 0;
五、常见多线程问题
1. 死锁
- 产生条件:互斥、占有且等待、非抢占、循环等待
- 解决方案:
- 按固定顺序获取锁
- 使用
std::lock()同时锁定多个互斥量 - 使用
std::scoped_lock(C++17)
2. 竞态条件
- 解决方案:正确使用同步机制保护共享数据
3. 虚假唤醒
- 条件变量等待时应使用谓词检查
六、性能考虑
- 锁粒度:尽量减小临界区范围
- 锁争用:减少线程对锁的竞争
- 无锁编程:在适当场景使用原子操作
- 缓存友好性:注意伪共享(false sharing)问题
七、C++20新增特性
1. 信号量(Semaphore)
#include <semaphore>
std::counting_semaphore<10> sem(0);
sem.acquire(); // P操作
sem.release(); // V操作
2. 闩(Latch)和屏障(Barrier)
#include <latch>
std::latch completion_latch(10); // 等待10个线程
completion_latch.arrive_and_wait();
#include <barrier>
std::barrier sync_point(5); // 5个线程的同步点
sync_point.arrive_and_wait();
八、最佳实践
- 优先使用高级抽象(
std::async,std::future等) - 避免裸锁,使用RAII包装器
- 尽量减少共享数据
- 使用线程安全的数据结构
- 注意异常安全
九、调试与测试
- 使用线程分析工具(如Valgrind的Helgrind)
- 编写确定性测试
- 使用静态分析工具检测潜在问题
温故知新
一、join和detach区别
在 C++ 中,std::thread 是用来表示线程的标准库类。每个 std::thread 对象都与一个线程相关联。当线程结束时,有两种处理方式:join 和 detach。这两种方法处理线程生命周期的不同方式:
join
等待线程结束:调用 join() 方法会等待线程自然结束。直到线程执行完毕,调用 join() 的线程才会继续执行。
资源清理:join() 会确保线程的资源被适当地清理。一旦线程结束,std::thread 对象将不再与任何线程关联。
用途:当需要确保线程执行完毕后再继续执行其他任务时,使用 join() 是合适的。
detach
分离线程:调用 detach() 方法会将线程与 std::thread 对象分离。std::thread 对象将不再控制线程,线程将继续执行直到完成。
资源管理:一旦线程被分离,线程的资源(如堆栈)将由操作系统在线程结束时自动清理。但是,如果线程尝试访问其所属程序的全局或静态变量,这些变量可能已经被销毁,因为 std::thread 对象可能已经销毁了。
用途:当不需要等待线程结束,或者线程执行的是长时间运行的任务,而主线程需要继续执行其他任务时,使用 detach() 是合适的。
二、原子操作和内存顺序
std::atomic 是 C++11 引入的一个用于原子操作的模板类,主要用于多线程环境下进行无锁的线程安全操作。它能够确保对数据的读取和写入操作是原子的,避免了线程间的竞争条件和数据不一致的问题。
total_time += stats->total_time.load(std::memory_order_relaxed)
std::memory_order_relaxed 是用于指定原子操作的内存顺序
内存顺序控制着不同线程间操作的可见性和执行顺序。C++11 引入了原子操作和内存顺序的概念,以支持多线程并发编程中的同步问题
s :这表示不做任何同步保证,操作仅仅是原子的。也就是说,这个操作不会影响其他操作的顺序,但它会确保原子操作本身是正确的。其他线程可能在看到这个操作的结果之前已经执行了其它操作,因此不适用于需要同步或排序的场景。
std::memory_order_acquire:表示当前操作之前的所有读写操作必须在当前操作完成后才能执行。这常用于获取锁或同步信息。
std::memory_order_release:表示当前操作之后的所有读写操作必须在当前操作之前执行。
std::memory_order_seq_cst:表示操作顺序遵循严格的顺序,确保在多线程环境下所有操作的执行顺序一致。
内存顺序模型对比表
内存顺序 保证内容 性能 典型使用场景
memory_order_relaxed 仅保证原子性,无顺序保证 最高 计数器、不需要同步的标志位
memory_order_acquire 保证后续操作不会被重排到它之前 较高 读取共享数据前的同步
memory_order_release 保证前面的操作不会被重排到它之后 较高 写入共享数据后的同步
memory_order_seq_cst 全局顺序一致性,所有线程看到相同的操作顺序 最低 需要严格顺序保
关键区别总结
同步保证:
relaxed:无同步
acquire-release:配对线程间同步
seq_cst:全局同步
典型模式:
生产者-消费者:生产者用release,消费者用acquire
互斥锁:加锁相当于acquire,解锁相当于release。
acquire:加载操作(读)load,release:存储操作(写)
计数器:可用relaxed
需要严格顺序时用seq_cst
使用 memory_order_acquire 确保状态读取时能看到最新值
使用 memory_order_release 确保状态更新能被其他线程看到
三、多线程之间如何通信的,进程之间如何通信的
多线程通信:线程之间可以通过共享内存、互斥锁、条件变量、信号量和消息队列等方式进行通信。
进程通信(IPC):进程之间由于各自独立的内存空间,通常通过管道、共享内存、消息队列、套接字、信号等方式进行通信。
四、进程切换和线程切换的区别
进程切换和线程切换是操作系统中两种不同的上下文切换机制,它们的区别主要体现在资源占用、切换开销、通信方式和应用场景上。以下是详细对比:
1. 基本概念
(1) 进程(Process)
-
定义:进程是资源分配的基本单位,拥有独立的地址空间、全局变量、文件描述符、子进程等资源。
-
特点:
- 进程间相互隔离,一个进程崩溃不会直接影响其他进程。
- 进程间通信(IPC)需要通过显式机制(如管道、消息队列、共享内存等)。
(2) 线程(Thread)
-
定义:线程是CPU调度的基本单位,共享进程的资源(如地址空间、全局变量、文件描述符等),但拥有独立的栈、程序计数器、寄存器等上下文。
-
特点:
- 线程间通信更高效(直接共享内存)。
- 一个线程崩溃可能导致整个进程崩溃(除非有隔离机制)。
2. 切换开销对比
(1) 进程切换的开销
-
需要切换的资源:
- 地址空间:切换页表(TLB flush),导致缓存失效。
- 内核资源:如打开的文件、信号处理表、内存映射等。
- CPU上下文:寄存器、程序计数器、栈指针等。
-
开销来源:
- TLB刷新:地址空间切换后,TLB(转换后备缓冲器)中的缓存失效,需重新加载页表。
- 内核态切换:进程切换通常涉及从用户态到内核态的切换(如通过系统调用或中断)。
- 缓存失效:CPU缓存(L1/L2/L3)中的数据可能不再有效。
-
典型时间:微秒级到毫秒级(取决于硬件和操作系统优化)。
(2) 线程切换的开销
-
需要切换的资源:
- CPU上下文:寄存器、程序计数器、栈指针等(与进程切换相同部分)。
- 线程局部存储(TLS) :部分实现可能需要切换TLS。
-
无需切换的资源:
- 地址空间:同一进程内的线程共享地址空间,无需切换页表。
- 内核资源:如文件描述符、全局变量等已共享。
-
开销来源:
- 寄存器保存/恢复:与进程切换类似,但无TLB刷新和缓存失效。
- 调度竞争:多线程可能因锁竞争导致额外开销。
-
典型时间:纳秒级到微秒级(比进程切换快10~100倍)。