目录
[1. 线程库的底层逻辑:C++11 vs Linux pthread](#1. 线程库的底层逻辑:C++11 vs Linux pthread)
[2. 第一个线程程序](#2. 第一个线程程序)
[3. 核心概念:join vs detach](#3. 核心概念:join vs detach)
[4. 线程同步基础:解决数据竞争](#4. 线程同步基础:解决数据竞争)
[1. std::mutex:互斥锁(最常用)](#1. std::mutex:互斥锁(最常用))
[2. std::condition_variable:条件变量(线程间通信)](#2. std::condition_variable:条件变量(线程间通信))
[3. std::atomic:原子操作(无锁同步)](#3. std::atomic:原子操作(无锁同步))
[1. C++11 线程库与 pthread 的区别?](#1. C++11 线程库与 pthread 的区别?)
[2. 死锁的原因及避免方法?](#2. 死锁的原因及避免方法?)
[3. 条件变量的虚假唤醒是什么?如何避免?](#3. 条件变量的虚假唤醒是什么?如何避免?)
[4. std::lock_guard vs std::unique_lock?](#4. std::lock_guard vs std::unique_lock?)
[5. 原子操作的实现原理?为什么比互斥锁快?](#5. 原子操作的实现原理?为什么比互斥锁快?)
[6. 线程局部存储(TLS)的实现与应用?](#6. 线程局部存储(TLS)的实现与应用?)
[7. 易错点](#7. 易错点)
[1. 实战 1:工业级线程池实现(muduo 风格)](#1. 实战 1:工业级线程池实现(muduo 风格))
[2. 实战 2:高性能同步:读写锁(std::shared_mutex)](#2. 实战 2:高性能同步:读写锁(std::shared_mutex))
[3. 实战 3:Linux 线程优化:亲和性 + 栈大小](#3. 实战 3:Linux 线程优化:亲和性 + 栈大小)
[4. 调试技巧](#4. 调试技巧)
[1. 查看线程信息:pstack/ps](#1. 查看线程信息:pstack/ps)
[2. GDB 调试多线程](#2. GDB 调试多线程)
[3. 检测数据竞争:tsan](#3. 检测数据竞争:tsan)
在 Linux C++ 后端开发中,线程库 是实现高并发、高性能服务的核心工具 ------C++11 引入的
std::thread系列接口封装了 Linux 底层的pthread库,既保留了系统级的高效性,又提供了现代 C++ 的类型安全和易用性。
一、基础核心
1. 线程库的底层逻辑:C++11 vs Linux pthread
Linux 系统层面的线程由pthread(POSIX 线程)库实现,而 C++11 的<thread>头文件是对pthread的类型安全封装------ 二者的核心对应关系如下:
| C++11 线程库组件 | 底层 Linux pthread 接口 | 核心作用 |
|---|---|---|
std::thread |
pthread_create/pthread_join/pthread_detach |
线程创建、启动、销毁 |
std::mutex |
pthread_mutex_t/pthread_mutex_lock/unlock |
互斥锁,保护临界区 |
std::condition_variable |
pthread_cond_t/pthread_cond_wait/signal |
线程间条件等待 / 通知 |
std::atomic |
atomic内置函数(x86:lock指令) |
无锁原子操作 |
std::thread_local |
__thread(GCC 扩展) |
线程局部存储(TLS) |
核心认知 :C++11 线程库是 "跨平台封装",在 Linux 下最终会调用pthread系统调用,因此编译时需链接-lpthread库。
2. 第一个线程程序
核心语法:std::thread的基础用法
cpp
#include <iostream>
#include <thread> // C++11线程库头文件
#include <unistd.h> // Linux sleep函数
// 线程函数:普通函数
void thread_func(int num, const std::string& msg) {
for (int i = 0; i < 3; ++i) {
// 输出线程ID + 传入参数
std::cout << "Thread " << num << " msg: " << msg
<< " (tid: " << std::this_thread::get_id() << ")" << std::endl;
sleep(1); // 模拟耗时操作
}
}
int main() {
// 1. 创建线程:传入函数+参数
std::thread t1(thread_func, 1, "Hello Linux");
std::thread t2(thread_func, 2, "C++ Thread");
// 2. 等待线程结束(join):主线程阻塞,直到子线程完成
t1.join();
t2.join();
// 3. 主线程继续执行
std::cout << "Main thread (tid: " << std::this_thread::get_id() << ") exit" << std::endl;
return 0;
}
编译运行(Linux)
bash
# 必须链接pthread库,-std=c++11及以上
g++ -std=c++11 thread_basic.cpp -o thread_basic -lpthread
./thread_basic
输出示例
bash
Thread 1 msg: Hello Linux (tid: 140709260797696)
Thread 2 msg: C++ Thread (tid: 140709252404992)
Thread 1 msg: Hello Linux (tid: 140709260797696)
Thread 2 msg: C++ Thread (tid: 140709252404992)
Thread 1 msg: Hello Linux (tid: 140709260797696)
Thread 2 msg: C++ Thread (tid: 140709252404992)
Main thread (tid: 140709268926336) exit
3. 核心概念:join vs detach
std::thread创建后必须调用join()或detach(),否则主线程结束时会触发std::terminate()(程序 abort),二者的核心区别如下:
| 操作 | 核心语义 | 线程归属 | 风险点 | 适用场景 |
|---|---|---|---|---|
join() |
主线程阻塞,等待子线程完成 | 归主线程管理 | 主线程阻塞过久,影响响应 | 需等待子线程结果、资源回收 |
detach() |
子线程与主线程分离,后台运行 | 归系统管理(守护线程) | 子线程访问的变量可能悬空 | 后台日志、监控等无需等待的任务 |
错误示例:未调用join/detach
cpp
#include <iostream>
#include <thread>
void func() { sleep(1); }
int main() {
std::thread t(func);
// 未调用t.join()或t.detach()
return 0; // 程序abort,输出:terminate called without an active exception
}
正确示例:detach的安全用法
cpp
#include <iostream>
#include <thread>
#include <unistd.h>
int main() {
// 捕获全局/静态变量,避免局部变量悬空
std::thread t([]() {
for (int i = 0; i < 5; ++i) {
std::cout << "Detached thread running..." << std::endl;
sleep(1);
}
});
t.detach(); // 分离线程
std::cout << "Main thread exit" << std::endl;
sleep(6); // 主线程等待足够久,确保子线程完成
return 0;
}
4. 线程同步基础:解决数据竞争
多线程访问共享变量时会出现数据竞争(未定义行为),必须用同步原语保护临界区,小白先掌握 3 个核心同步工具:
1. std::mutex:互斥锁(最常用)
核心作用:保证同一时间只有一个线程进入临界区,示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <unistd.h>
int count = 0; // 共享变量
std::mutex mtx; // 全局互斥锁
void increment() {
for (int i = 0; i < 10000; ++i) {
// 加锁:RAII方式,自动解锁(避免死锁)
std::lock_guard<std::mutex> lock(mtx);
count++; // 临界区:仅一个线程能执行
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "count = " << count << std::endl; // 正确输出20000
return 0;
}
2. std::condition_variable:条件变量(线程间通信)
核心作用:让线程等待某个条件满足,避免忙等(CPU 空转),示例:生产者消费者模型(简化版):
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <unistd.h>
std::queue<int> q; // 共享队列
std::mutex mtx;
std::condition_variable cv;
// 生产者:生产数据
void producer() {
for (int i = 1; i <= 5; ++i) {
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
std::cout << "Produced: " << i << std::endl;
cv.notify_one(); // 通知消费者
sleep(1);
}
}
// 消费者:消费数据
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待条件:队列非空(避免虚假唤醒)
cv.wait(lock, []() { return !q.empty(); });
int val = q.front();
q.pop();
std::cout << "Consumed: " << val << std::endl;
lock.unlock(); // 提前解锁,减少阻塞
if (val == 5) break; // 消费完退出
}
}
int main() {
std::thread t_prod(producer);
std::thread t_cons(consumer);
t_prod.join();
t_cons.join();
return 0;
}
3. std::atomic:原子操作(无锁同步)
核心作用:对简单类型(int/long/ 指针)的操作是原子的,无需加锁,性能更高,示例:
cpp
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> count = 0; // 原子变量
void increment() {
for (int i = 0; i < 1000000; ++i) {
count++; // 原子操作,无数据竞争
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "atomic count = " << count << std::endl; // 正确输出2000000
return 0;
}
二、补充知识点
1. C++11 线程库与 pthread 的区别?
- 封装层级 :
std::thread是 C++ 标准库,封装了 Linux 的pthread系统调用,跨平台(Windows/Linux);pthread是 Linux 专属的 C 语言接口,无跨平台性; - 类型安全 :
std::thread支持任意可调用对象(Lambda / 仿函数 / 成员函数),编译期检查参数类型;pthread仅支持 C 函数指针,类型检查弱; - 异常处理 :
std::thread抛出 C++ 异常,可捕获;pthread通过返回码传递错误,需手动检查; - 编译链接 :
std::thread仍需链接-lpthread(Linux 下),本质依赖pthread。
2. 死锁的原因及避免方法?
- 死锁原因 :四个条件同时满足:
- 互斥:资源只能被一个线程持有;
- 持有并等待:线程持有资源 A,等待资源 B;
- 不可剥夺:资源不能被强制收回;
- 循环等待:线程 1 等线程 2 的资源,线程 2 等线程 1 的资源。
- 避免方法 :
- 按固定顺序加锁(如先锁 mtx1,再锁 mtx2);
- 使用
std::lock一次性加多个锁; - 使用
std::timed_mutex设置超时,避免永久等待; - 最小化锁的持有时间。
3. 条件变量的虚假唤醒是什么?如何避免?
虚假唤醒 :线程被notify_one()唤醒后,条件可能已不满足(如多个消费者被唤醒,只有一个能拿到数据);
避免方法:
- 用
cv.wait(lock, 条件判断),唤醒后重新检查条件; - 循环检查条件:
cpp
while (q.empty()) { cv.wait(lock); }
4. std::lock_guard vs std::unique_lock?
| 特性 | std::lock_guard |
std::unique_lock |
|---|---|---|
| 锁管理 | RAII,构造加锁,析构解锁 | RAII,支持手动加锁 / 解锁 |
| 灵活性 | 低(不可手动解锁) | 高(lock()/unlock()) |
| 性能 | 无额外开销 | 轻微额外开销(状态管理) |
| 适用场景 | 简单临界区,全程加锁 | 条件变量、提前解锁、超时锁 |
| 配合条件变量 | 不支持 | 必须用(cv.wait()需要解锁) |
5. 原子操作的实现原理?为什么比互斥锁快?
- 原理 :原子操作通过 CPU 的原子指令 (如 x86 的
lock前缀)实现,保证指令执行期间总线锁定,其他 CPU 核心无法访问该内存; - 比互斥锁快的原因 :
- 互斥锁涉及内核态 / 用户态切换(上下文开销);
- 原子操作是用户态指令,无上下文切换,仅锁定总线(纳秒级开销);
- 原子操作无阻塞(自旋),互斥锁可能导致线程挂起。
6. 线程局部存储(TLS)的实现与应用?
- 实现 :Linux 下 C++11 的
std::thread_local封装了__thread(GCC 扩展),每个线程有独立的变量副本,存储在线程私有栈 / 数据区; - 应用场景 :
- 线程独立的计数器、日志 ID;
- 避免函数静态变量的线程安全问题;
- 数据库连接池的线程私有连接。
示例:thread_local的用法
cpp
#include <iostream>
#include <thread>
#include <unistd.h>
thread_local int tls_num = 0; // 线程局部变量
void func(int id) {
tls_num = id; // 每个线程修改自己的副本
for (int i = 0; i < 3; ++i) {
std::cout << "Thread " << id << " tls_num: " << tls_num << std::endl;
sleep(1);
}
}
int main() {
std::thread t1(func, 1);
std::thread t2(func, 2);
t1.join();
t2.join();
return 0;
}
7. 易错点
- 误区 1 :
std::mutex可以递归加锁 → 错!std::mutex是非递归锁,重复加锁会死锁,需用std::recursive_mutex(工业级慎用,易隐藏逻辑问题); - 误区 2:原子操作适用于所有场景 → 错!原子操作仅支持简单类型的简单操作(++/--/ 赋值),复杂逻辑仍需互斥锁;
- 误区 3 :
detach后线程可访问局部变量 → 错!detach的线程若访问局部变量,变量销毁后会悬空(未定义行为); - 误区 4 :条件变量的
notify_one一定会唤醒线程 → 错!若notify_one在wait前调用,线程会永久等待(需初始化条件); - 误区 5:多线程一定比单线程快 → 错!线程切换有开销(约 1-10μs),高频小任务的多线程可能比单线程慢(需用线程池优化)。
三、线程库的实践
1. 实战 1:工业级线程池实现(muduo 风格)
线程池是 Linux 高并发服务的核心组件,解决 "频繁创建销毁线程" 的开销问题,核心设计要点:
- 任务队列 + 工作线程池;
- 优雅关闭(处理剩余任务);
- 移动语义优化任务拷贝;
- 避免数据竞争。
完整代码
cpp
#include <iostream>
#include <functional>
#include <queue>
#include <vector>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <future>
#include <utility>
// 任务类型:包装任意可调用对象,支持返回值
using Task = std::function<void()>;
class ThreadPool {
private:
std::vector<std::thread> workers_; // 工作线程
std::queue<Task> tasks_; // 任务队列
std::mutex mtx_; // 保护任务队列
std::condition_variable cv_; // 任务通知
std::atomic<bool> running_; // 线程池运行状态
size_t max_workers_; // 最大线程数
public:
// 构造函数:初始化线程池
explicit ThreadPool(size_t max_workers = std::thread::hardware_concurrency())
: running_(true), max_workers_(max_workers) {
// 创建工作线程
for (size_t i = 0; i < max_workers_; ++i) {
workers_.emplace_back([this]() {
while (running_) {
Task task;
// 加锁取任务
{
std::unique_lock<std::mutex> lock(mtx_);
// 等待任务或停止信号
cv_.wait(lock, [this]() {
return !running_ || !tasks_.empty();
});
// 停止且无任务,退出线程
if (!running_ && tasks_.empty()) return;
// 取任务(移动语义,避免拷贝)
task = std::move(tasks_.front());
tasks_.pop();
}
// 执行任务(解锁后执行,减少阻塞)
task();
}
});
}
}
// 析构函数:优雅关闭线程池
~ThreadPool() {
running_ = false;
cv_.notify_all(); // 唤醒所有线程
// 等待所有线程结束
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
}
// 禁用拷贝和移动
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
ThreadPool(ThreadPool&&) = delete;
ThreadPool& operator=(ThreadPool&&) = delete;
// 添加任务:支持任意参数,返回future获取结果
template <typename F, typename... Args>
auto add_task(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
// 包装任务为std::packaged_task,支持返回值
using ReturnType = decltype(f(args...));
auto task = std::make_shared<std::packaged_task<ReturnType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
// 获取future,供调用方获取结果
std::future<ReturnType> fut = task->get_future();
// 加锁添加任务
{
std::lock_guard<std::mutex> lock(mtx_);
if (!running_) {
throw std::runtime_error("add task to stopped ThreadPool");
}
// 封装为Task,加入队列
tasks_.emplace([task]() { (*task)(); });
}
cv_.notify_one(); // 唤醒一个线程执行任务
return fut;
}
};
// 测试:任务函数(带返回值)
int sum(int a, int b) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return a + b;
}
int main() {
ThreadPool pool(4); // 4个工作线程
// 添加10个任务,获取future
std::vector<std::future<int>> results;
for (int i = 0; i < 10; ++i) {
results.emplace_back(pool.add_task(sum, i, i*2));
}
// 输出任务结果
for (auto& fut : results) {
std::cout << "Result: " << fut.get() << std::endl;
}
return 0;
}
编译运行(Linux)
cpp
g++ -std=c++17 thread_pool.cpp -o thread_pool -lpthread
./thread_pool
亮点
- 类型安全 :支持任意返回值的任务,通过
std::future获取结果; - 性能优化:移动语义减少任务拷贝,解锁后执行任务减少阻塞;
- 优雅关闭:析构时处理剩余任务,避免资源泄漏;
- 异常安全:禁止拷贝 / 移动,避免线程池被非法复制。
2. 实战 2:高性能同步:读写锁(std::shared_mutex)
Linux 下读多写少的场景(如配置读取、缓存),用std::shared_mutex(C++17)实现读写锁,读并发、写互斥,性能远超普通互斥锁:
cpp
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
#include <unistd.h>
std::shared_mutex rw_mtx; // 读写锁
int config = 10; // 共享配置
// 读线程:共享加锁,允许多个读
void read_config(int id) {
for (int i = 0; i < 3; ++i) {
std::shared_lock<std::shared_mutex> lock(rw_mtx);
std::cout << "Reader " << id << " read config: " << config << std::endl;
lock.unlock();
sleep(1);
}
}
// 写线程:独占加锁,仅一个写
void write_config(int new_val) {
std::unique_lock<std::shared_mutex> lock(rw_mtx);
std::cout << "Writer update config to: " << new_val << std::endl;
config = new_val;
sleep(2);
}
int main() {
// 5个读线程,1个写线程
std::vector<std::thread> readers;
for (int i = 1; i <= 5; ++i) {
readers.emplace_back(read_config, i);
}
std::thread writer(write_config, 20);
// 等待所有线程
for (auto& t : readers) { t.join(); }
writer.join();
return 0;
}
3. 实战 3:Linux 线程优化:亲和性 + 栈大小
工业级场景需优化线程性能,Linux 下可调整线程的核心亲和性(绑定 CPU)和栈大小:
cpp
#include <iostream>
#include <thread>
#include <pthread.h> // Linux pthread接口
// 设置线程亲和性:绑定到指定CPU核心
void set_thread_affinity(std::thread& t, int cpu_core) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_core, &cpuset);
pthread_t tid = t.native_handle(); // 获取pthread_t
pthread_setaffinity_np(tid, sizeof(cpu_set_t), &cpuset);
}
// 设置线程栈大小
void set_thread_stack_size(std::thread& t, size_t stack_size) {
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, stack_size);
// 需在创建线程前设置,此处仅示例
pthread_attr_destroy(&attr);
}
void func() {
while (true) { sleep(1); }
}
int main() {
std::thread t(func);
// 绑定线程到CPU 0核心(Linux下核心编号从0开始)
set_thread_affinity(t, 0);
std::cout << "Thread bound to CPU 0" << std::endl;
t.join();
return 0;
}
4. 调试技巧
1. 查看线程信息:pstack/ps
bash
# 查看进程的所有线程
ps -T -p <pid>
# 查看线程调用栈
pstack <pid>
2. GDB 调试多线程
bash
gdb ./thread_pool
(gdb) info threads # 查看所有线程
(gdb) thread <id> # 切换到指定线程
(gdb) bt # 查看线程调用栈
(gdb) set scheduler-locking on # 锁定线程调度,避免切换
3. 检测数据竞争:tsan
bash
# 编译时启用tsan,检测数据竞争
g++ -std=c++17 thread.cpp -o thread -lpthread -fsanitize=thread
./thread # 输出数据竞争的详细信息
四、避坑指南
- 坑点 1 :线程捕获局部变量的引用(
detach后悬空)解决方案 :值捕获变量,或用std::shared_ptr管理资源; - 坑点 2 :互斥锁未解锁(异常导致)解决方案 :始终用 RAII 锁(
lock_guard/unique_lock),避免手动解锁; - 坑点 3 :条件变量未处理虚假唤醒解决方案 :用带谓词的
wait,或循环检查条件; - 坑点 4 :线程池任务队列满(内存溢出)解决方案 :限制队列大小,
add_task时阻塞或返回错误; - 坑点 5 :原子变量的复杂操作(如
a += b)非原子解决方案 :用std::atomic::fetch_add等原子操作函数,或加锁。
- 优先使用线程池:避免频繁创建销毁线程(Linux 下线程创建开销约 1ms);
- 最小化锁粒度:仅在临界区加锁,提前解锁;
- 读多写少用读写锁 :
std::shared_mutex比std::mutex性能高 5-10 倍; - 简单操作用原子变量:避免互斥锁的上下文切换开销;
- 线程数控制:CPU 密集型任务线程数 = CPU 核心数,IO 密集型任务线程数 = CPU 核心数 * 2;
- 优雅关闭:线程池 / 服务退出时,处理剩余任务,释放资源。
五、总结
- 基础层 :
std::thread封装 Linuxpthread,必须调用join/detach,编译链接-lpthread; - 同步层 :互斥锁(
mutex)保护临界区,条件变量(condition_variable)实现线程通信,原子操作(atomic)优化简单同步; - 实战层:线程池是工业级高并发的核心,读写锁优化读多写少场景,线程亲和性提升缓存命中率;
- 避坑层:避免数据竞争、死锁、悬空引用,用 RAII 管理锁,处理虚假唤醒。