C++ 多线程
一、基本概念
1. 线程与多任务
- 线程:进程内的轻量级执行单元,是程序最小执行流。
- 进程:资源分配的最小单位,每个进程独立内存空间。
- 多任务分类
- 基于进程:多个独立程序并发运行。
- 基于线程:同一个程序内部多个执行流并发运行。
2. 线程资源特性
- 同进程内共享:全局变量、堆内存、文件句柄、地址空间。
- 每个线程独有:栈空间、寄存器、程序计数器、局部变量。
3. 并发与并行
- 并发:单核CPU时间片轮转,交替执行多个线程,宏观同时、微观串行。
- 并行:多核CPU同一时刻多个线程物理上同时执行。
二、C++11 多线程标准库头文件
C++11 引入跨平台标准线程库,无需依赖系统API(Windows CreateThread / Linux pthread):
| 头文件 | 作用 |
|---|---|
<thread> |
std::thread 线程创建、管理 |
<mutex> |
互斥量、锁管理 |
<condition_variable> |
条件变量,线程等待与唤醒 |
<future> |
std::promise / std::future 线程结果传递 |
<atomic> |
原子类型、无锁并发 |
编译命令(Linux):
bash
g++ -std=c++11 main.cpp -o main -lpthread
三、四种创建线程方式
1. 普通全局函数 / 普通函数
cpp
#include <iostream>
#include <thread>
using namespace std;
void taskFunc(int num)
{
for (int i = 0; i < num; ++i)
{
cout << "子线程执行:" << i << endl;
}
}
int main()
{
// 创建并启动线程
thread t1(taskFunc, 3);
// 等待线程结束
t1.join();
return 0;
}
2. 仿函数(重载()运算符的类)
cpp
#include <iostream>
#include <thread>
using namespace std;
class Task
{
public:
void operator()(int cnt)
{
for (int i = 0; i < cnt; ++i)
{
cout << "仿函数线程执行:" << i << endl;
}
}
};
int main()
{
thread t2(Task(), 3);
t2.join();
return 0;
}
3. Lambda 表达式创建线程
cpp
#include <iostream>
#include <thread>
using namespace std;
int main()
{
thread t3([](int cnt){
for (int i = 0; i < cnt; ++i)
{
cout << "Lambda线程执行:" << i << endl;
}
}, 3);
t3.join();
return 0;
}
4. 类成员函数创建线程
注意:必须传对象地址 + 成员函数地址
cpp
#include <iostream>
#include <thread>
using namespace std;
class Work
{
public:
void run(int n)
{
for (int i = 0; i < n; ++i)
{
cout << "成员函数线程:" << i << endl;
}
}
};
int main()
{
Work w;
// 格式:线程对象(类成员函数地址, 对象地址, 参数)
thread t4(&Work::run, &w, 3);
t4.join();
return 0;
}
四、线程核心操作:join / detach
1. join()
- 主线程阻塞等待子线程执行完毕。
- 线程结束后自动回收资源。
- 同一个线程只能
join一次。
cpp
thread t(taskFunc);
t.join();
2. detach() 分离线程
- 把子线程从主线程分离,后台独立运行。
- 主线程不再等待,也不能再
join。 - 主线程退出会直接结束整个进程,分离线程也会被强行终止。
cpp
thread t(taskFunc);
t.detach();
3. joinable() 判断线程状态
判断线程是否还可以 join:
cpp
if (t.joinable())
{
t.join();
}
禁止直接销毁
joinable状态的线程对象,会直接程序崩溃。
4. 获取线程属性
cpp
// 获取当前线程ID
this_thread::get_id();
// 获取CPU核心支持的并发线程数
thread::hardware_concurrency();
五、线程传参详解
1. 默认值传递
线程构造函数会拷贝参数到线程内部。
2. 引用传递 std::ref
普通传参无法引用,必须用 std::ref 包装:
cpp
#include <iostream>
#include <thread>
using namespace std;
void add(int& a)
{
a += 10;
}
int main()
{
int x = 5;
thread t(add, ref(x));
t.join();
cout << x << endl; // 15
return 0;
}
3. 指针传递
直接传递变量地址即可,注意变量生命周期不能提前结束。
4. 线程捕获引用陷阱
Lambda 直接引用局部变量,若主线程先退出,子线程会访问野指针。
六、线程安全与互斥量 mutex
多线程读写共享全局变量会引发数据竞争、结果错乱,需要加锁。
1. std::mutex 基础互斥锁
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx;
int g_val = 0;
void safeAdd()
{
for (int i = 0; i < 100000; ++i)
{
mtx.lock(); // 加锁
g_val++;
mtx.unlock(); // 解锁
}
}
int main()
{
thread t1(safeAdd);
thread t2(safeAdd);
t1.join();
t2.join();
cout << g_val << endl;
return 0;
}
2. std::lock_guard 自动锁
作用域托管,出作用域自动解锁 ,无需手动 lock/unlock:
cpp
void safeAdd()
{
for (int i = 0; i < 100000; ++i)
{
lock_guard<mutex> lg(mtx);
g_val++;
}
}
3. std::unique_lock 灵活锁
比 lock_guard 更灵活:支持手动解锁、延迟加锁、锁转移:
cpp
unique_lock<mutex> ul(mtx);
// 手动解锁
ul.unlock();
// 重新加锁
ul.lock();
七、条件变量 condition_variable
作用:线程等待、条件满足再唤醒,常用于生产者-消费者模型。
核心搭配:
std::condition_variablestd::unique_lock<std::mutex>wait()等待、notify_one()唤醒一个、notify_all()唤醒全部
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
mutex mtx;
condition_variable cv;
bool ready = false;
void consumer()
{
unique_lock<mutex> lk(mtx);
// 等待条件为true
cv.wait(lk, [](){ return ready; });
cout << "子线程被唤醒,开始工作" << endl;
}
void producer()
{
this_thread::sleep_for(chrono::seconds(1));
lock_guard<mutex> lk(mtx);
ready = true;
cv.notify_one(); // 唤醒一个等待线程
}
int main()
{
thread t1(consumer);
thread t2(producer);
t1.join();
t2.join();
return 0;
}
八、原子操作 std::atomic
无需互斥锁,底层硬件指令保证变量原子性,效率更高。
cpp
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> g_cnt{0};
void addTask()
{
for (int i = 0; i < 100000; ++i)
{
g_cnt++;
}
}
int main()
{
thread t1(addTask);
thread t2(addTask);
t1.join();
t2.join();
cout << g_cnt << endl;
return 0;
}
九、线程局部存储 thread_local
thread_local 修饰的变量每个线程独有一份,互不干扰,天然线程安全:
cpp
#include <iostream>
#include <thread>
using namespace std;
thread_local int num = 100;
void func(int id)
{
num += id;
cout << "线程" << id << " num = " << num << endl;
}
int main()
{
thread t1(func, 1);
thread t2(func, 2);
t1.join();
t2.join();
return 0;
}
十、std::promise 与 std::future 线程传值
用于子线程向主线程返回结果:
std::promise:子线程设置值std::future:主线程获取值,get()会阻塞等待
cpp
#include <iostream>
#include <thread>
#include <future>
using namespace std;
void setValue(promise<int> p)
{
p.set_value(666); // 设置结果
}
int main()
{
promise<int> p;
future<int> f = p.get_future();
thread t(setValue, move(p));
// 阻塞等待获取子线程结果
int res = f.get();
cout << "收到子线程结果:" << res << endl;
t.join();
return 0;
}
十一、死锁产生与解决
1. 死锁产生四个必要条件
- 互斥占有
- 不可剥夺
- 请求保持
- 循环等待
2. 常见死锁场景
多个线程互相持有对方需要的锁,都不释放,互相永久等待。
3. 死锁避免方案
- 统一锁的申请顺序,所有线程按相同顺序加锁。
- 减少嵌套互斥锁。
- 使用
std::lock一次性锁定多个互斥量。 - 加锁设置超时时间。
十二、多线程编程通用注意事项
- 共享全局变量、静态变量必须加锁或用
atomic。 - 分离线程
detach要保证依赖资源生命周期不提前销毁。 - 不能对同一个线程多次
join。 - 线程对象析构前必须
join或detach,否则程序崩溃。 - 类成员函数作为线程入口,必须传递对象地址。
- 多线程尽量少用全局变量,优先
thread_local或消息队列通信。