文章目录
- 一、前言
-
- [1.1 什么是并发](#1.1 什么是并发)
- [1.2 为什么使用并发](#1.2 为什么使用并发)
- [1.3 并发与C++多线程](#1.3 并发与C++多线程)
- 二、线程基础
-
- [2.1 发起线程](#2.1 发起线程)
- [2.2 等待线程完成](#2.2 等待线程完成)
- [2.3 lambda表达式传递](#2.3 lambda表达式传递)
- [2.4 在后台运行线程](#2.4 在后台运行线程)
- [2.5 向线程传递参数](#2.5 向线程传递参数)
一、前言
1.1 什么是并发
同一个系统中,多个独立活动同时进行,而非依次进行。举个生活中的例子:你一边煮饭一边洗菜,虽然你的手不能同时做两件事,但你可以一会儿切菜、一会儿看锅,整体上两项任务都在推进------这就是并发。
并发的方式有两种:
(1)多进程并发。例如将一个应用软件拆分成多个独立进程同时运行,它们都只含单一线程,非常类似于同时运行浏览器和文字处理软件
(2)多线程并发。线程非常像轻量级进程:每个线程都独立运行,并能各自执行不同的指令序列
注意:并发 ≠ 并行:
并发:多个任务交替执行,可能在单核 CPU 上通过时间片轮转实现。
并行:多个任务真正同时执行,通常需要多核 CPU。
1.2 为什么使用并发
- 为分离关注点而并发:归类相关代码,隔离无关代码,使程序更易于理解和测试,因此所含缺陷很可能更少。例如一个GUI应用中,主线程:负责响应用户界面事件(点击、拖拽等),必须保持高响应性。工作线程:执行耗时操作(如文件读写、网络请求、复杂计算)
- 为性能而并发:现代 CPU 通常有多个核心,单线程程序只能利用一个核心,而并发程序可以将任务拆分,在多个核心上真正并行执行,从而缩短总执行时间。再者,很多程序性能瓶颈不在 CPU,而在 I/O 操作(如磁盘读写、网络请求)。这些操作往往需要等待外部设备响应,在此期间 CPU 是空闲的。通过并发,一个线程等待 I/O 时,其他线程可以继续工作,系统整体吞吐量显著提升
1.3 并发与C++多线程
C++98 / C++03:没有线程的年代
🚫 标准中无任何线程概念
- C++98 和 C++03 标准完全没有定义线程、锁、原子操作等并发原语。
所有多线程编程必须依赖操作系统 API: - Windows:CreateThread, CriticalSection
- POSIX(Linux/macOS):pthread_create, pthread_mutex_t
- 代码不可移植,且容易出错(如忘记释放锁、资源泄漏)。
⚠️ 内存模型缺失
- C++98 没有明确定义内存模型,即多个线程如何观察共享内存的修改顺序。
- 编译器优化(如指令重排)可能导致多线程程序行为不可预测。
- 即使使用 volatile,也无法保证原子性或同步。
!里程碑:C++11
随着C++11标准的发布,上述种种弊端被一扫而空。C++标准库不仅规定了内存模型,可以区分不同线程,还扩增了新类,分别用于线程管控、保护共享数据、同步线程间操作、以及底层原子操作等。
C++14进一步增添了对并发和并行的支持,具体而言,是引入了一种用于保护共享数据的新互斥。C++17则增添了一系列适合新手的并行算法函数。这两版标准都强化了C++的核心和标准程序库的其他部分,简化了多线程代码的编写
二、线程基础
2.1 发起线程
线程通过构建std::thread对象而启动,该对象指明线程要运行的任务。最简单的任务就是运行一个普通函数,返回空,也不接收参数。函数在自己的线程上运行,等它一返回,线程即随之终止。
cpp
#include <thread>
void thead_work1() {
std::cout << "hello thread " << std::endl;
}
// 通过()初始化并启动一个线程
std::thread t1(thead_work1);
2.2 等待线程完成
当我们启动一个线程后,线程可能没有立即执行,如果在局部作用域启动了一个线程,或者main函数中,很可能子线程没运行就被回收了,回收时会调用线程的析构函数,执行terminate操作。所以为了防止主线程退出或者局部作用域结束导致子线程被析构的情况,我们可以通过join,让主线程等待子线程启动运行,子线程运行结束后主线程再运行。
cpp
#include <thread>
void thead_work1() {
std::cout << "hello thread " << std::endl;
}
int main(){
std::thread t1(thead_work1);
ti.join(); // 使用join
return 0;
}
2.3 lambda表达式传递
cpp
std::thread t4([](std::string str) {
std::cout << "str is " << str << std::endl;
}, hellostr);
t4.join();
2.4 在后台运行线程
调用std::thread对象的成员函数detach(),会令线程在后台运行,遂无法与之直接通信。假若线程被分离,就无法等待它完结,也不可能获得与它关联的std::thread对象,因而无法汇合该线程。然而分离的线程确实仍在后台运行,其归属权和控制权都转移给C++运行时库,由此保证,一旦线程退出,与之关联的资源都会被正确回收。
cpp
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
假设我们有一个程序,希望启动一个后台线程持续写日志,而主线程继续做其他事情,不需要等待日志线程结束:
cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <fstream>
void background_logger() {
std::ofstream log("app.log");
int count = 0;
while (count < 10) {
log << "Log entry #" << ++count << "\n";
log.flush(); // 确保立即写入
std::this_thread::sleep_for(std::chrono::seconds(1));
}
log << "Logger finished.\n";
// 函数返回,线程自然结束
}
int main() {
std::cout << "Main: Starting background logger...\n";
std::thread logger_thread(background_logger);
// 将线程分离:让它在后台独立运行
logger_thread.detach();
// 主线程继续工作
std::this_thread::sleep_for(std::chrono::milliseconds(3500));
std::cout << "Main: Doing other work...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(8000));
std::cout << "Main: Exiting program.\n";
// 注意:如果主线程在此退出,而 logger_thread 还没结束,
// 程序会终止,后台线程也会被强制杀死!
}
detach() 后主线程不等待
主线程打印完 "Exiting program." 就结束,不会等日志线程写完 10 条日志。
如果主线程结束,整个进程退出,所有线程(都会被操作系统强制终止。
所以上例中 app.log 可能只写了 3~4 行,而不是完整的 10 行
| 问题 | 说明 |
|---|---|
| 访问已销毁的栈变量 | 如果线程函数捕获了主线程的局部变量(尤其是引用或指针),而主线程已退出,会导致悬空指针! |
| 无法处理异常 | detached 线程中的未捕获异常会导致 std::terminate(),且无法被主线程感知。 |
| 资源泄漏风险 | 如果线程持有资源(如文件句柄、锁),提前终止可能导致资源未释放。 |
2.5 向线程传递参数
当线程要调用的回调函数参数为引用类型时,需要将参数显示转化为引用对象传递给线程的构造函数,如果采用如下调用会编译失败
cpp
void producer_thread(ThreadSafeQueue<int>& tsq)
{
for (int i=0; i < 100; i++)
tsq.push(i);
}
void run()
{
ThreadSafeQueue<int> tsq;
std::thread t_pro(producer_thread, tsq);
}
即使函数change_param的参数为int&类型,我们传递给t2的构造函数为some_param,也不会达到在change_param函数内部修改关联到外部some_param的效果。因为some_param在传递给thread的构造函数后会转变为右值保存,右值传递给一个左值引用会出问题,所以编译出了问题。需要使用std::ref
cpp
void run()
{
ThreadSafeQueue<int> tsq;
std::thread t_pro(producer_thread, std::ref(tsq));
}
有时候传递给线程的参数是独占的,所谓独占就是不支持拷贝赋值和构造,但是我们可以通过 std::move 的方式将参数的所有权转移给线程,如下
cpp
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));
在调用std::thread的构造函数时,依据std::move§所指定的操作,big_object对象的归属权会发生转移,先进入新创建的线程的内部存储空间,再转移给process_big_object()函数