[特殊字符] C++ 线程与多线程:从基础到实战(附完整代码示例)
本文基于 C++11 标准( std::thread 为核心),覆盖线程基础、同步机制、常见问题与最佳实践,所有代码均可直接编译运行。
一、核心概念:线程与多线程
1. 基础定义
- 线程(Thread):操作系统调度的最小执行单元,是进程内的独立执行流,共享进程的内存空间(堆、全局变量、代码段),拥有独立的栈空间、寄存器、程序计数器。
- 多线程(Multithreading):在一个进程中同时运行多个线程,利用多核CPU并行执行任务,提升程序效率与响应速度。
- 核心优势:
- 并行处理耗时任务(如IO、计算),避免UI阻塞
- 充分利用多核CPU资源,提升吞吐量
- 模块化拆分任务,简化复杂逻辑
2. 线程 vs 进程
| 特性 | 线程 | 进程 |
|---|---|---|
| 内存空间 | 共享进程内存 | 独立内存空间 |
| 切换开销 | 小(仅切换上下文) | 大(需切换页表、内存空间) |
| 通信方式 | 共享内存、互斥锁、条件变量 | 管道、消息队列、共享内存(需同步) |
| 安全性 | 线程崩溃会导致整个进程崩溃 | 进程崩溃互不影响 |
| 资源占用 | 低 | 高 |
简单总结:
- 线程是进程内的执行单元,共享资源,通信简单但隔离性差
- 进程是独立的执行实体,资源隔离好但开销大
- 选择时需权衡通信效率和安全性隔离的需求
二、C++ 线程基础: std::thread 核心用法
C++11 引入 头文件,提供 std::thread 类用于创建和管理线程,是标准库的核心线程组件。
1. 线程的创建与启动
std::thread 构造函数可接收函数、lambda、仿函数、成员函数等可调用对象,创建后立即启动线程。
示例1:基础线程创建(函数 + lambda)
#include <iostream>
#include <thread>
#include <chrono>
// 普通函数作为线程入口
void thread_func(int id, const std::string& msg) {
for (int i = 0; i < 3; ++i) {
std::cout << "Thread " << id << ": " << msg << " (count: " << i << ")\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 线程休眠500ms
}
}
int main() {
// 1. 创建线程1:绑定普通函数
std::thread t1(thread_func, 1, "Hello from thread 1");
// 2. 创建线程2:绑定lambda表达式
std::thread t2([](sslocal://flow/file_open?url=int+id&flow_extra=eyJsaW5rX3R5cGUiOiJjb2RlX2ludGVycHJldGVyIn0=) {
for (int i = 0; i < 3; ++i) {
std::cout << "Lambda Thread " << id << ": Running...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
}, 2);
// 3. 主线程任务
std::cout << "Main thread: Waiting for child threads...\n";
// 4. 等待子线程执行完毕(必须调用,否则线程销毁时崩溃)
t1.join();
t2.join();
std::cout << "Main thread: All threads finished!\n";
return 0;
}
关键知识点:
- std::this_thread::sleep_for :让当前线程休眠指定时间,释放CPU资源。
- join() :阻塞主线程,等待子线程执行完毕后再继续,是线程安全销毁的核心方法。
- detach() :将线程与 std::thread 对象分离,线程在后台独立运行,不可再调用 join() ,需确保线程任务执行完毕前进程不退出(否则线程被强制终止)。
- 线程参数传递: std::thread 会拷贝参数到线程栈中,若需传递引用,需用 std::ref 包装(否则会拷贝对象,导致引用失效)。
示例2:引用参数传递( std::ref )
void update_value(int& val) {
val += 10;
std::cout << "Thread: val = " << val << "\n";
}
int main() {
int num = 5;
// 必须用std::ref传递引用,否则会拷贝num,原变量不会被修改
std::thread t(update_value, std::ref(num));
t.join();
std::cout << "Main: val = " << num << "\n"; // 输出15
return 0;
}
示例3:成员函数作为线程入口
#include <iostream>
#include <thread>
class Worker {
public:
void do_work(int id) {
std::cout << "Worker " << id << " is working...\n";
}
};
int main() {
Worker w;
// 成员函数作为线程入口:第一个参数为成员函数指针,第二个为对象指针/引用
std::thread t(&Worker::do_work, &w, 1);
t.join();
return 0;
}
2. 线程的常用方法
| 方法 | 功能 |
|---|---|
join() |
阻塞等待线程执行完毕,线程对象销毁前必须调用(否则崩溃) |
detach() |
分离线程,线程在后台运行,不可再join |
joinable() |
判断线程是否可join(未join、未detach的线程可join) |
get_id() |
获取线程ID(类型为thread::id,用于标识线程) |
native_handle() |
获取原生线程句柄(用于调用系统API,如pthread接口) |
this_thread::get_id() |
获取当前线程的ID |
this_thread::yield() |
提示调度器让出当前时间片 |
this_thread::sleep_for() |
让当前线程休眠指定时长 |
this_thread::sleep_until() |
让当前线程休眠到指定时间点 |
使用注意事项:
- 线程对象在析构前必须处于非运行状态(
join()完成)或已分离(detach()) - 分离后的线程资源由系统自动回收,程序无法再控制其生命周期
- 线程ID在线程结束后可能被系统复用,不宜作为唯一标识长期存储
示例4:线程ID与joinable判断
#include <iostream>
#include <thread>
void thread_func() {
std::cout << "Child thread ID: " << std::this_thread::get_id() << "\n";
}
int main() {
std::thread t(thread_func);
std::cout << "Main thread ID: " << std::this_thread::get_id() << "\n";
std::cout << "Child thread object ID: " << t.get_id() << "\n";
std::cout << "Is t joinable? " << t.joinable() << "\n"; // 输出1(true)
t.join();
std::cout << "After join, is t joinable? " << t.joinable() << "\n"; // 输出0(false)
return 0;
}
三、多线程同步:解决数据竞争与线程安全
多线程的核心问题是数据竞争(Race Condition):多个线程同时读写共享资源,导致数据错乱。C++ 提供多种同步机制解决该问题。
1. 互斥锁: std::mutex 与 std::lock_guard
std::mutex 是最基础的互斥锁,用于保护共享资源,保证同一时间只有一个线程进入临界区。
std::lock_guard 是RAII风格的锁管理器,自动加锁/解锁,避免手动解锁遗漏导致死锁。
示例5:无锁 vs 有锁的对比(数据竞争问题)
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
// 共享资源
int counter = 0;
std::mutex mtx; // 互斥锁
// 无锁版本:数据竞争,结果错误
void unsafe_increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 非原子操作,多线程同时读写导致数据错乱
}
}
// 有锁版本:线程安全,结果正确
void safe_increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // RAII自动加锁,作用域结束自动解锁
counter++;
}
}
int main() {
// 测试无锁版本
counter = 0;
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(unsafe_increment);
}
for (auto& t : threads) t.join();
std::cout << "Unsafe counter: " << counter << "\n"; // 结果远小于1000000,随机错误
// 测试有锁
2893137936: 04-09 22:45:24
counter = 0;
threads.clear();
for (int i = 0; i < 10; ++i) {
threads.emplace_back(safe_increment);
}
for (auto& t : threads) t.join();
std::cout << "Safe counter: " << counter << "\n"; // 结果恒为1000000,正确
return 0;
}
关键知识点:
- std::mutex 核心方法:
- lock() :加锁,若锁被占用则阻塞等待
- unlock() :解锁,需手动调用(易遗漏,推荐用 lock_guard )
- try_lock() :尝试加锁,成功返回true,失败返回false,不阻塞
- std::lock_guard :
- 构造时自动加锁,析构时自动解锁,完全避免死锁
- 不可拷贝、不可移动,作用域内有效
- std::unique_lock :比 lock_guard 更灵活,支持手动加锁/解锁、延迟加锁、转移所有权,适合复杂场景(如条件变量)。
2. 递归互斥锁: std::recursive_mutex
普通 std::mutex 不可递归加锁(同一线程重复加锁会导致死锁), std::recursive_mutex 支持同一线程多次加锁,需对应次数解锁。
示例6:递归锁使用场景
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex rmtx;
void recursive_func(int depth) {
std::lock_guard<std::recursive_mutex> lock(rmtx);
std::cout << "Depth: " << depth << "\n";
if (depth > 0) {
recursive_func(depth - 1); // 同一线程递归加锁,不会死锁
}
}
int main() {
std::thread t(recursive_func, 3);
t.join();
return 0;
}
3. 条件变量: std::condition_variable
用于线程间的等待-通知机制,让线程在条件不满足时阻塞等待,条件满足时被唤醒,避免轮询浪费CPU。
必须配合 std::unique_lock 使用。
示例7:生产者-消费者模型(条件变量经典场景)
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
std::queue<int> buffer; // 共享缓冲区
std::mutex mtx;
std::condition_variable cv;
const int MAX_BUFFER_SIZE = 5;
// 生产者线程:生产数据放入缓冲区
void producer(int id) {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 缓冲区满时,阻塞等待消费者消费
cv.wait(lock, { return buffer.size() < MAX_BUFFER_SIZE; });
int data = id * 100 + i;
buffer.push(data);
std::cout << "Producer " << id << " produced: " << data << "\n";
lock.unlock();
cv.notify_one(); // 唤醒一个等待的消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// 消费者线程:从缓冲区取数据
void consumer(int id) {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 缓冲区空时,阻塞等待生产者生产
cv.wait(lock, { return !buffer.empty(); });
int data = buffer.front();
buffer.pop();
std::cout << "Consumer " << id << " consumed: " << data << "\n";
lock.unlock();
cv.notify_one(); // 唤醒一个等待的生产者
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
}
int main() {
std::thread p1(producer, 1);
std::thread c1(consumer, 1);
std::thread c2(consumer, 2);
p1.join();
c1.join();
c2.join();
return 0;
}
关键知识点:
- wait(lock, predicate) :阻塞等待,直到 predicate 返回true(避免虚假唤醒)
- **notify_one() :**唤醒一个等待的线程
- **notify_all() :**唤醒所有等待的线程
- 虚假唤醒:线程被唤醒但条件不满足,因此必须用 predicate 二次判断
4. 原子操作: std::atomic
对于简单的共享变量(如计数器、标志位),用原子操作替代互斥锁,性能更高,无锁开销。
std::atomic 是C++11引入的原子类型,保证操作的原子性、可见性、有序性。
示例8:原子计数器(替代互斥锁)
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> counter(0); // 原子计数器,初始值0
void atomic_increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 原子操作,线程安全,无需锁
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(atomic_increment);
}
for (auto& t : threads) t.join();
std::cout << "Atomic counter: " << counter << "\n"; // 恒为1000000,正确
return 0;
常用原子操作:
- load() :读取原子变量值
- store() :写入原子变量值
- fetch_add() :原子加法( counter++ 等价于 fetch_add(1) )
- fetch_sub() :原子减法
- exchange() :原子交换值
- compare_exchange_strong() :比较并交换(CAS操作,无锁编程核心)
四、多线程常见问题与最佳实践
1. 常见错误与解决方案
| 错误/问题 | 主要原因 | 推荐解决方案 |
|---|---|---|
| 线程销毁时崩溃 | 未调用 join()/detach(),线程对象销毁时线程仍在运行 | 线程对象销毁前必须调用 join()(推荐)或 detach() |
| 数据竞争 | 多个线程同时读写共享资源,未加同步 | 用互斥量/读写锁保护共享资源 |
| 死锁 | 多个线程互相持有对方需要的锁,无限等待 | 按固定顺序加锁、用 scoped_lock 同时加锁、避免嵌套锁 |
| 虚假唤醒 | 条件变量被唤醒但条件不满足 | 必须将等待放在 while 循环中进行二次条件判断 |
| UI阻塞/无响应 | 主线程执行耗时任务,未用子线程 | 耗时任务放子线程,UI更新切回主线程(Qt 推荐用信号槽机制) |
使用建议: 在多线程编程中,建议遵循 RAII 原则管理资源(如使用 lock_guard),在 Qt 中充分利用信号槽的线程安全特性进行跨线程通信,并尽量减少共享状态,采用消息传递模式来降低复杂度。
2. 死锁示例与解决
死锁示例:
std::mutex mtx1, mtx2;
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(mtx2); // 等待mtx2,被thread2持有
}
void thread2() {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock1(mtx1); // 等待mtx1,被thread1持有
}
**解决方案1:固定加锁顺序**
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::lock_guard<std::mutex> lock2(mtx2); // 先mtx1后mtx2
}
void thread2() {
std::lock_guard<std::mutex> lock1(mtx1);
std::lock_guard<std::mutex> lock2(mtx2); // 相同顺序,无死锁
}
解决方案2: std::lock 同时加锁
void thread1() {
std::lock(mtx1, mtx2); // 同时加锁,避免顺序问题
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock); // 接管已加锁的mtx1
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock); // 接管已加锁的mtx2
}
void thread2() {
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
}
3. 多线程最佳实践
1. 优先用 std::thread ,避免原生线程API(如pthread):标准库跨平台、更安全。
2. 用RAII锁( std::lock_guard / std::unique_lock ),禁止手动 lock() / unlock() :避免死锁。
3. 简单共享变量用 std::atomic ,复杂资源用 std::mutex :平衡性能与安全性。
4. 线程池替代频繁创建销毁线程:线程创建/销毁开销大,用线程池复用线程(C++20引入 std::jthread ,C++17可自己实现)。
5. 避免线程间共享大量数据:减少同步开销,降低复杂度。
6. 线程数量与CPU核心数匹配:过多线程会导致上下文切换开销,一般设为 std::thread::hardware_concurrency() (获取CPU核心数)。
五、C++20 新特性: std::jthread (自动join的线程)
C++20 引入 std::jthread ( std::thread 的改进版),核心优势:
- 析构时自动调用 join() ,无需手动 join() ,彻底避免线程销毁崩溃
- 内置 std::stop_source ,支持线程安全的停止请求
示例9: std::jthread 基础用法
#include <iostream>
#include <thread>
#include <chrono>
void thread_func(std::stop_token st) {
while (!st.stop_requested()) { // 检查是否收到停止请求
std::cout << "jthread running...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "jthread stopped!\n";
}
int main() {
std::jthread t(thread_func); // 自动启动,无需手动join
std::this_thread::sleep_for(std::chrono::seconds(3));
t.request_stop(); // 发送停止请求
// 析构时自动join,无需手动调用
return 0;
}
六、Qt 线程补充(结合你的项目场景)
你之前的Qt项目中用 QThread ,与 std::thread 的核心区别:
- QThread 是Qt封装的线程类,继承自 QObject ,支持信号槽、事件循环
- QThread 推荐用 moveToThread 模式(将工作对象移到线程中),而非重写 run()
- QMutex 是Qt的互斥锁,与 std::mutex 功能一致,Qt项目中优先用 QMutex
Qt 线程安全示例(静态锁+成员线程)
// thread.h
#ifndef THREAD_H
#define THREAD_H
#include <QThread>
#include <QMutex>
class Thread : public QThread {
Q_OBJECT
public:
static int num;
static QMutex mutex; // 静态锁,所有实例共享
void stop();
protected:
void run() override;
private:
std::atomic<bool> m_running{true};
};
#endif
// thread.cpp
int Thread::num = 0;
QMutex Thread::mutex;
void Thread::stop() { m_running = false; }
void Thread::run() {
while (m_running) {
QMutexLocker locker(&mutex); // RAII锁,自动加锁/解锁
num++;
qDebug() << "Thread" << QThread::currentThreadId() << "num:" << num;
msleep(100);
}
}
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "thread.h"
namespace Ui { class Widget; }
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget() override;
protected:
void closeEvent(QCloseEvent *event) override;
private:
Ui::Widget *ui;
Thread thread1, thread2; // 成员线程,生命周期与Widget一致
};
#endif
// widget.cpp
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
thread1.start();
thread2.start();
}
Widget::~Widget() { delete ui; }
void Widget::closeEvent(QCloseEvent *event) {
thread1.stop();
thread2.stop();
thread1.wait();
thread2.wait();
event->accept();
}
七、编译与运行
所有代码需支持C++11及以上标准,编译命令(GCC):
bash
g++ -std=c++11 thread_demo.cpp -o thread_demo -pthread
./thread_demo
( -pthread 是必须的,用于链接线程库)
八、总结
- 线程基础: std::thread 是核心, join() 是线程安全销毁的关键。
- 同步机制: std::mutex 保护共享资源, std::condition_variable 实现线程通信, std::atomic 用于简单原子操作。
- **最佳实践:**RAII锁、避免死锁、线程池、匹配CPU核心数。
- Qt 适配: QThread 用成员变量+静态锁,重写 closeEvent 实现安全退出。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)