C++ 多线程知识汇总
https://zhuanlan.zhihu.com/p/194198073 (防链接失效)
程序使用并发的原因有两种:
- 为了关注点分离(程序中不同的功能,使用不同的线程去执行),当为了分离关注点而使用多线程时,设计线程的数量的依据,不再是依赖于 CPU 中的可用内核的数量,而是依据概念上的设计(依据功能的划分);
- 为了提高性能, 此时线程数量可以依据CPU的逻辑核心数目,这样可以使得每个线程能在不同的CPU核心上同时并发执行;
知道何时不使用并发与知道何时使用它一样重要。
不使用并发的唯一原因就是收益(性能的增幅)比不上成本(代码开发的脑力成本、时间成本,代码维护相关的额外成本)。运行越多的线程,操作系统需要为每个线程分配独立的栈空间,需要越多的上下文切换,这会消耗很多操作系统资源,如果在线程上的任务完成得很快,那么实际执行任务的时间要比启动线程的时间小很多,所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能,此时收益就比不上成本。而且多线程代码如果编写不当,运行中会出现很多问题,诸如执行结果不符合预期、程序崩溃等问题。
产生死锁的四个必要条件(面试考点):
- 互斥(资源同一时刻只能被一个进程使用)
- 请求并保持(进程在请资源时,不释放自己已经占有的资源)
- 不剥夺(进程已经获得的资源,在进程使用完前,不能强制剥夺)
- 循环等待(进程间形成环状的资源循环等待关系)
临界区速度最快,但只能作用于同一进程下不同线程,不能作用于不同进程;临界区可确保某一代码段同一时刻只被一个线程执行;
信号量多个线程同一时刻访问共享资源,进行线程的计数,确保同时访问资源的线程数目不超过上限,当访问数超过上限后,不发出信号量;
std::unique_lock 类似于 lock_guard,只是 std::unique_lock 用法更加丰富,同时支持 std::lock_guard() 的原有功能。 使用 std::lock_guard 后不能手动l ock() 与手动 unlock(),使用 std::unique_lock 后可以手动 lock() 与手动 unlock(); std::unique_lock 的第二个参数,除了可以是 adopt_lock,还可以是try_to_lock 与 defer_lock;
try_to_lock: 尝试去锁定,得保证锁处于 unlock 的状态,然后尝试现在能不能获得锁;尝试用 mutx 的 lock() 去锁定这个 mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,并继续往下执行;
defer_lock: 始化了一个没有加锁的 mutex;
std::condition_variable
是 C++ 中用于线程间同步的一个重要工具,可以让一个线程在某个条件不满足时等待,直到另一个线程通知它。
#include <iostream> #include <thread> #include <condition_variable> #include <queue> #include <atomic> std::mutex mtx; // 互斥量 std::condition_variable cv; // 条件变量 std::queue<int> dataQueue; // 共享数据队列 std::atomic<bool> done(false); // 指示生产者是否完成 void producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产延迟 { std::lock_guard<std::mutex> lock(mtx); // 锁定互斥量 dataQueue.push(i); // 生产数据 std::cout << "Produced: " << i << std::endl; } cv.notify_one(); // 通知一个等待的线程 } done = true; // 标记生产结束 cv.notify_all(); // 通知所有等待的线程 } void consumer() { while (true) { std::unique_lock<std::mutex> lock(mtx); // 锁定互斥量 cv.wait(lock, [] { return !dataQueue.empty() || done; }); // 等待条件变量 if (dataQueue.empty() && done) { break; // 如果没有数据且生产已完成,退出循环 } int data = dataQueue.front(); // 访问共享资源 dataQueue.pop(); std::cout << "Consumed: " << data << std::endl; } } int main() { std::thread prod(producer); // 启动生产者线程 std::thread cons(consumer); // 启动消费者线程 prod.join(); // 等待生产者线程结束 cons.join(); // 等待消费者线程结束 return 0; }
为了减少创建与销毁线程所带来的时间消耗与资源消耗,因此采用线程池的策略:
程序启动后,预先创建一定数量的线程放入空闲队列中,这些线程都是处于阻塞状态,基本不消耗 CPU,只占用较小的内存空间。
接收到任务后,任务被挂在任务队列,线程池选择一个空闲线程来执行此任务。
任务执行完毕后,不销毁线程,线程继续保持在池中等待下一次的任务。
线程池所解决的问题:
(1) 需要频繁创建与销毁大量线程的情况下,由于线程预先就创建好了,接到任务就能马上从线程池中调用线程来处理任务,减少了创建与销毁线程带来的时间开销和CPU资源占用。
(2) 需要并发的任务很多时候,无法为每个任务指定一个线程(线程不够分),使用线程池可以将提交的任务挂在任务队列上,等到池中有空闲线程时就可以为该任务指定线程。