c++_学习笔记【02】_多线程编程基础
c++_学习笔记【02】——多线程
一:前置知识储备
1 学习多线程有关的c++基础
1.1函数对象
1.1.1 介绍
函数对象(Functor)是重载了() 运算符的类/结构体。也叫仿函数——它本质是一个对象,但是可以像普通函数一样被调用(通过 obj() 或 obj(args))。
特点:
1 兼具对象和函数的特性:可以存储状态(类的成员变量),这是普通函数做不到的。
2 可调用性:语法上和普通函数、lambda 表达式、函数指针统一,属于 C++ 的 “可调用对象” 范畴。
3 性能优势:编译器可内敛优化,比含函数针更高效(无直接调用开销)。
1.1.2 函数对象应用
#include<iostream>
#include<vector>
#include<algorithm>
struct Functor01
{
int sum = 0; // 成员变量:存储累加状态
void operator()(int num) // 重载()运算符:编译期确定的成员函数
{
sum += num;
}
};
int main()
{
std::vector<int> nums = { 1,2,3,4,5 };
Functor01 f1_;
// 编译期确定调用Functor01::operator(),直接内联函数体
f1_(10);
std::cout << "f1_.sum=" << f1_.sum << std::endl;
//template <class InputIt, class UnaryFunction>
//UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f);
f1_ = std::for_each(nums.begin(), nums.end(), f1_);
std::cout << "f1_.sum=" << f1_.sum << std::endl;
Functor01 f2_; //结果独立
f2_(3);
std::cout << "f2_.sum=" << f2_.sum << std::endl;
}
运行结果:

1.1.3 函数对象在多线程中应用
#include<iostream>
#include<thread>
#include<mutex>
// 带状态的仿函数:线程安全的累加器
struct Functor01 {
int sum = 0;
std::mutex mtx; // 互斥锁,保护sum的读写
void operator()(int num)
{
std::lock_guard<std::mutex> lock(mtx);
sum += num;
std::cout << "线程ID:" << std::this_thread::get_id()
<< ",当前sum=" << sum << std::endl;
}
};
int main()
{
Functor01 fun_;
// 传引用!否则线程会拷贝fun_,修改的是副本
std::thread thread01(std::ref(fun_), 3);
std::thread thread02(std::ref(fun_), 30);
std::thread thread03(std::ref(fun_), 300);
thread01.join();
thread02.join();
thread03.join();
std::cout << "多线程累加后sum=" << fun_.sum << std::endl;
return 0;
}
运行结果:

1.2 lambda表达式
1.2.1 介绍
Lambda表达式是C++11引入的匿名函数,是C++ 多线程编程中最常用的可调用对象,也是std::thread/std::async等工具的首选入口逻辑。
特点:
1 作为线程入口函数 :std::thread的构造函数接收任意 “可调用对象”,Lambda 因无需提前定义函数 / 函数对象,成为多线程中定义线程逻辑的最优选择 —— 尤其适合简单、一次性的线程任务。
2 Lambda 在多线程中的核心优势
- 就地定义逻辑:线程要执行的逻辑直接写在 Lambda 里,代码可读性更高(无需跳转到其他函数);
- 灵活捕获上下文:可直接捕获外部变量(如共享数据、配置参数),无需通过全局变量 / 指针传参(减少错误);
- 语法简洁:相比函数对象,无需定义类、重载
()运算符,代码量大幅减少。
语法:
[capture](parameters) mutable -> return_type {
// 函数体
}
各部分说明:
| 作用 | |
|---|---|
[capture] |
捕获列表:指定从外部作用域捕获哪些变量(值 / 引用)到 lambda 内部使用 |
(parameters) |
参数列表:和普通函数的参数一致(可省略,无参数时可空) |
mutable |
可选:允许修改按值捕获的变量(默认按值捕获的变量是 const 的) |
-> return_type |
可选:显式指定返回类型(编译器通常能自动推导,可省略) |
{} |
函数体:具体执行逻辑 |
常见捕获方式
| 捕获语法 | 含义 |
|---|---|
[] |
空捕获:不捕获任何外部变量 |
[=] |
按值捕获:捕获所有外部变量(副本) |
[&] |
按引用捕获:捕获所有外部变量(引用) |
[x, &y] |
按值捕获 x,按引用捕获 y |
[this] |
捕获当前类的 this 指针(类成员函数中) |
1.2.2 注意事项
多线程中使用 Lambda 的关键注意事项
-
捕获方式的选择
- 按值捕获(
[=]/[x]):Lambda 内部是变量的副本,线程间互不影响(无需加锁),但无法修改外部变量; - 按引用捕获(
[&]/[&x]):Lambda 内部直接操作外部变量,必须加锁保护共享变量(避免数据竞争); - 禁止捕获局部变量的引用后,线程脱离变量生命周期(如主线程的局部变量被子线程引用,主线程先退出会导致野引用)。
- 按值捕获(
-
避免隐式捕获的坑
- 尽量避免
[&](捕获所有引用),显式指定需要捕获的变量(如[&num, &mtx]),减少误捕获导致的问题; - 类成员函数中,Lambda 捕获
this指针后,需确保对象生命周期长于线程(避免对象销毁后线程访问this)。
- 尽量避免
-
mutable 的使用
std::thread t([x]() mutable { x += 5; // 加mutable后可修改副本 std::cout << x << std::endl; });
1.2.3 结合多线程基础用法
无捕获的 Lambda 作为线程入口
#include<iostream>
#include<thread>
int main()
{
std::thread thread01([] {
int num = 10;
std::cout << "线程ID" << std::this_thread::get_id() << ",10的立方"
<< num * num * num << std::endl;
});
thread01.join();
return 0;
}
运行结果:

1.2.4 结合多线程捕获外部变量
捕获外部变量(按值 / 按引用)
#include<iostream>
#include<mutex>
#include<thread>
int main()
{
int share_sum = 0;
std::mutex mtx;
//按引用捕获shared_num和mtx,累加10
std::thread thread01([&share_sum, &mtx]() {
std::lock_guard<std::mutex> lock(mtx);
share_sum += 10;
std::cout << "线程ID:" << std::this_thread::get_id() << " share_sum="
<< share_sum << std::endl;
});
std::thread thread02([&]() { // [&]捕获所有外部变量(引用)
std::lock_guard<std::mutex> lock(mtx);
share_sum += 20;
std::cout << "线程ID:" << std::this_thread::get_id() << " share_sum="
<< share_sum << std::endl;
});
thread01.join();
thread02.join();
return 0;
}
运行结果:

1.2.5 结合 std::async
结合 std::async(异步获取结果)
std::async 是 C++ 标准库提供的异步任务封装器,底层本质是对 std::thread + 同步原语(互斥锁、条件变量) + 异常处理的封装,核心优势是 “简化线程管理”。
注意:
fut.get() 是阻塞式调用 ,主线程可以在异步任务执行期间做自己的事,直到需要结果时才调用fut.get() 阻塞等待。
在看下面代码前进行概念补充 :
wait_for() 非阻塞检查任务状态的三种方法:
-
std::future_status::ready:任务已完成,可安全调用get(); -
std::future_status::timeout:任务未完成,主线程继续执行(输出日志); -
std::future_status::deferred:任务延迟执行(本示例显式指定async策略,不会触发)。代码:
#include<iostream>
#include<thread>
#include<future>
#include<chrono>
int cala_sum(int a, int b)
{
std::this_thread::sleep_for(std::chrono::seconds(2));// 模拟耗时2秒
return a, b;
}
int main()
{
std::future<int> fut = std::async(cala_sum, 10, 30);
int result;
while (true)
{
// 非阻塞检查:等待200毫秒,返回任务状态
std::future_status status = fut.wait_for(std::chrono::milliseconds(200));
if (status == std::future_status::ready)// 任务已完成
{
result = fut.get();
break;
}
else if (status == std::future_status::timeout)// 任务未完成,超时
{
std::cout << "主线程继续运行....." << std::endl;
}
else if(status == std::future_status::deferred)// 任务延迟执行(本示例不会触发)
{
std::cout << "任务延迟执行,立即调用get()..." << std::endl; // 任务延迟执行(本示例不会触发)
result = fut.get(); // 触发任务执行
break;
}
}
std::cout << "异步任务结束:结果=" << result << std::endl;
return 0;
}
运行结果


在任务完成时候结果就存入共享区域,当调用.get() 时候就会发生阻塞获取结果,如果该任务未完成则持续阻塞。
1.3 智能指针
1.3.1 介绍
C++智能指针是封装了原始指针的的类模板,核心目标是是解决手动释放内存的痛点,例如:内存泄漏、野指针、重复释放等问题。其底层依赖**RAII(资源获取即初始化)**机制,智能指针在对象的生命周期结束(析构)的时候自动释放所管理的内存,无需手动delete .
C++11 提供了三种核心智能指针,其中 std::unique_ptr 和 std::shared_ptr 是最常用的:
| 智能指针类型 | 核心特性 |
|---|---|
std::unique_ptr |
独占所有权:同一时间只有一个 unique_ptr 指向对象,不可拷贝(可移动)。 |
std::shared_ptr |
共享所有权:多个 shared_ptr 可指向同一对象,通过引用计数管理生命周期(计数为 0 时释放对象)。 |
std::weak_ptr |
弱引用:配合 shared_ptr 使用,不增加引用计数,解决循环引用问题(补充提及,因和 shared_ptr 强相关)。 |
1.3.2 unique_ptr
特点:
-
独占资源,不可拷贝(避免多个指针共同管理统一资源),但是可以通过
std::move()转移所有权 -
析构时自动释放所指向的内存,适合“一对一”管理资源的方式
-
支持指向数组(
std::unique_ptr<int[]>)。代码:
#include<iostream>
#include<memory>
class Test {
private:
int id_;
public:
Test(int id):id_(id)
{
std::cout << "构造函数,id=" << id_ <<std::endl;
}
~Test(){
std::cout << "析构函数,id=" << id_ << std::endl;
}
void printf()
{
std::cout << "id=" << id_ << std::endl;
}
};
int main()
{
// 1. 创建unique_ptr,独占管理Test对象
std::unique_ptr<Test> ptr1 = std::make_unique<Test>(1);
ptr1->printf();// 调用成员函数,语法和原始指针一致
// 2. 不可拷贝(编译错误)
// std::unique_ptr<Test> uptr2 = uptr1;
// 3. 移动所有权(通过std::move)
std::unique_ptr<Test> ptr2 = std::move(ptr1);
if (!ptr1)
{
std::cout << "ptr1为空" << std::endl;
}
ptr2->printf();
return 0;
}
运行结果:

1.3.3 share_ptr
特点:
- 共享所有权:多个
share_ptr可以指向同一个对象,内部维护用计数器.use_count() - 拷贝时计数器+1,析构时计数器-1,计数器为0时,自动释放对象
代码(共享所有权):
#include<iostream>
#include<memory>
#include<chrono>
class Test {
private:
int id_;
public:
Test(int id) :id_(id)
{
std::cout << "构造函数,id=" << id_ << std::endl;
}
~Test() {
std::cout << "析构函数,id=" << id_ << std::endl;
}
void printf()
{
std::cout << "id=" << id_ << std::endl;
}
};
int main()
{
std::shared_ptr<Test> ptr3 = std::make_shared<Test>(3);
std::cout << "引用计数ptr3:" << ptr3.use_count() << std::endl;
std::shared_ptr<Test> ptr4 = ptr3;
std::cout << "引用计数ptr3:" << ptr3.use_count() << std::endl;
std::cout << "引用计数ptr4:" << ptr4.use_count() << std::endl;
ptr3->printf();
ptr4->printf();
ptr3.reset();
/*reset() 还有重载版本,可用于 “重新绑定资源”
让ptr3放弃原有资源,重新指向新的Test(4)
ptr3.reset(new Test(4));*/
std::cout << "引用计数ptr3:" << ptr3.use_count() << std::endl;
std::cout << "引用计数ptr4:" << ptr4.use_count() << std::endl;
return 0;
}
运行结果:

1.3.4 weak_ptr
特点:
shared_ptr 的循环引用会导致计数器无法归0,内存泄漏。weak_ptr 是弱引用,不增加计数,可解决该问题。
代码:
#include<iostream>
#include<memory>
class B;
class A {
public:
std::weak_ptr<B> b_ptr;
~A() { std::cout << "A 析构" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr;
~B() { std::cout << "B 析构" << std::endl; }
};
int main()
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
return 0;
}
运行结果:

1.3.5 智能指针和多线程的关系
智能指针本身不是"线程安全"的,但是它是多线程编程中管理共享资源的工具——解决了多线程下内存管理的问题,同时需要界限
规则:
share_ptr的引用计数器是线程安全的:多个线程同事拷贝/析构同一个share_ptr,引用计数器的增减是原子操作的(无需加锁)sharea_ptr指向的对象不是线程安全的:多个线程读写线程本身,仍然需要加锁unique_ptr无限程安全保障:因为独占所有权,通常不会多个线程访问同一个unique_ptr,如果转移所有权需要用std::move()并加锁- 单个
sharea_ptr对象的修改也不是线程安全的:比如线程 A 修改sptr = nullptr,线程 B 同时读取sptr,结果未定义(需加锁)。
注意事项:
- 避免裸指针混用:多线程中不要同时使用智能指针和原始指针管理同一对象(比如
shared_ptr管理的对象,又被裸指针delete,导致重复释放); - make_shared 优先于 new:
make_shared一次性分配对象内存和引用计数内存,更高效,且避免内存泄漏(比如new后抛出异常,智能指针未创建); - shared_ptr 循环引用:多线程中若两个对象互相持有
shared_ptr,会导致内存泄漏,需用weak_ptr打破循环; - 线程间传递 unique_ptr:只能通过
std::move转移所有权,且需加锁保证队列 / 容器的线程安全; - 不要捕获悬空的智能指针:Lambda 中捕获
shared_ptr而非原始指针,避免对象销毁后线程访问野指针
1.3.6 生产者消费者问题
#include<iostream>
#include<memory>
#include<condition_variable>
#include<thread>
#include<chrono>
#include<mutex>
#include<queue>
std::mutex print_mtx;
template<typename T>
class ProducerConsumer {
private:
std::queue<T> buffer_;
std::size_t max_buffer_size_;
std::condition_variable cv_producer_;
std::condition_variable cv_consumer_;
std::mutex mtx_;
std::atomic<bool> is_running_;
public:
ProducerConsumer(size_t num) :max_buffer_size_(num),is_running_(true) {}
~ProducerConsumer()
{
stop();
}
void producer(const T& data, size_t producer_id)
{
std::unique_lock<std::mutex> lock_(mtx_);
// 等待条件:停止运行 或 缓冲区未满
cv_producer_.wait(lock_, [this]() {
return !is_running_ || buffer_.size() < max_buffer_size_;
});
if (!is_running_) return;
buffer_.push(data);
{
std::lock_guard<std::mutex> lock_print(print_mtx);
std::cout << "[生产者 " << producer_id << "] 生产数据:"
<< data << " 目前缓冲区大小:" << buffer_.size() << std::endl;
}
cv_consumer_.notify_one();
}
bool consumer(T& data, size_t consumer_id)
{
std::unique_lock<std::mutex> lock_(mtx_);
// 等待条件:停止运行 或 缓冲区非空
cv_consumer_.wait(lock_,[this]() {
return !is_running_ || !buffer_.empty();
});
if (!is_running_ && buffer_.empty()) return false;
data = buffer_.front();
buffer_.pop();
{
std::lock_guard<std::mutex> lock_print(print_mtx);
std::cout << "------[消费者 " << consumer_id << "] 消费数据:"
<< data << " 目前缓冲区大小:" << buffer_.size() << std::endl;
}
cv_producer_.notify_one();
return true;
}
void stop()
{
is_running_ = false;
// 唤醒所有阻塞的生产者/消费者,避免线程卡死
cv_consumer_.notify_all();
cv_producer_.notify_all();
}
};
// 生产者线程函数
void producer_test(ProducerConsumer<int> *pc , size_t producer_id )
{
for (int i = 0;i < 3;i++)
{
int data = producer_id * 100 + i;
pc->producer(data, producer_id);
// 模拟生产耗时
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
// 消费者线程函数
void consumer_test(ProducerConsumer<int>* pc, size_t consumer_id)
{
int data;
while (true)
{
if (!pc->consumer(data, consumer_id)) break;
// 模拟生产耗时
std::this_thread::sleep_for(std::chrono::milliseconds(800));
}
{
std::lock_guard<std::mutex> lock_print(print_mtx);
std::cout << "消费者 " << consumer_id << " 退出! " << std::endl;
}
}
int main()
{
ProducerConsumer<int> pc(3);
std::thread pro_thread01(producer_test, &pc, 1);
std::thread pro_thread02(producer_test, &pc, 2);
std::thread pro_thread03(producer_test, &pc, 3);
std::thread con_thread01(consumer_test, &pc, 1);
std::thread con_thread02(consumer_test, &pc, 2);
pro_thread01.join();
pro_thread02.join();
pro_thread03.join();
{
std::lock_guard<std::mutex> lock_print(print_mtx);
std::cout << "所有生产者完成生产,等待消费者处理剩余数据..." << std::endl;
}
pc.stop();
con_thread01.join();
con_thread02.join();
{
std::lock_guard<std::mutex> lock_print(print_mtx);
std::cout << "所有线程执行结束!" << std::endl;
}
return 0;
}
运行结果:

1.4 右值引用与移动语义
1.4.1 介绍
在前面的例子中已经用到移动语义,如果您读到这里应该了解其作用和用法,就不在过多介绍,下面强调注意事项和重点回忆复习:
关于概念您可参考我以前的文章:
https://blog.csdn.net/weixin_50214044/article/details/154123604?spm=1001.2014.3001.5501
左值(lvalue) 和右值(rvalue) —— 这是 C++11 引入移动语义的基础:
| 类型 | 核心特征 | 示例 |
|---|---|---|
| 左值 | 有名称、可取地址、生命周期较长(可在多条语句中使用) | int a = 10; 中的 a |
| 右值 | 无名称、不可取地址、生命周期短暂(仅在当前表达式有效) | 10、a+b、函数返回的临时对象 |
右值引用(&&) :专门绑定到右值的引用类型,语法为 T&&(区别于左值引用 T&);
移动语义: 传统的拷贝语义(拷贝构造函数/拷贝赋值运算符)会深拷贝对象的资源(如堆内存、文件句柄),开销大;移动语义对于临时对象(右值),无需拷贝资源,直接 “偷” 走其资源(转移所有权),原对象变为 “空壳”,大幅减少内存拷贝和析构开销;通过移动构造函数和移动赋值运算符(参数为右值引用 T&&)实现。
std::move() 将左值强制转换为右值引用(仅做类型转换,不移动任何数据),让左值也能触发移动语义;std::move 后,原对象的资源已被转移,禁止再使用原对象(除非重新赋值)。
注意事项:
-
资源唯一性:移动后原对象变为 “空壳”,禁止多线程同时访问原对象(否则会访问空资源,导致崩溃);
-
线程安全的移动:移动操作本身不是线程安全的 —— 若多个线程同时对一个对象执行
std::move或访问,需加锁保护; -
配合智能指针:
std::unique_ptr是移动语义的典型应用(不可拷贝,只能移动),多线程中传递unique_ptr必须用std::move; -
避免移动常量:
const对象无法触发移动语义(会退化为拷贝),因此移动的对象需是非const的; -
noexcept 关键字:移动构造 / 赋值需加
noexcept—— 标准容器(如std::vector)在扩容时,若移动操作不抛出异常,才会使用移动而非拷贝,多线程中更稳定。代码:(重写string函数)
#include<iostream> #include<cstring> #include<utility> using namespace std; class MyString { private: char* _data; size_t _size; size_t _capacity; void allocateAndCopy(const char* str, size_t len) { _size = len; _capacity = len + 1; _data = new char[_capacity]; strcpy_s(_data,_capacity, str); } public: //默认构造函数 MyString(const char* str = "") { size_t len = strlen(str); allocateAndCopy(str, len); cout << "默认构造函数:" << _data << endl; } //拷贝构造函数 MyString(const MyString& other) { allocateAndCopy(other._data, other._size); cout << "拷贝构造函数:" << _data << endl; } //拷贝赋值函数 MyString& operator=(const MyString& other) { allocateAndCopy(other._data, other._size); cout << "拷贝赋值函数:"<<_data << endl; return *this; } // 移动构造函数(浅拷贝资源 + 置空原对象,右值引用参数) MyString(MyString&& other) noexcept { _data = other._data; _size = other._size; _capacity = other._capacity; other._data = nullptr; other._size = 0; other._capacity = 0; cout << "移动构造函数:" << _data << endl; } //移动赋值运算符 MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] _data; _data = other._data; _size = other._size; _capacity = other._capacity; other._data = nullptr; other._size = 0; other._capacity = 0; } cout << "移动赋值运算符:" << _data << endl; return *this; } ~MyString() { cout << "析构:" << (_data ? _data : "nullptr" )<< endl; delete[] _data; } const char* c_str() const { return _data; } size_t size() const { return _size; } }; int main() { MyString s1("hello"); MyString s2 = s1; MyString s3(s1); MyString s4("world"); s4 = s1; MyString s5("temp1"); MyString s6 = move(s5); MyString s7("temp7"); s7 = move(s6); }运行结果:

2 操作系统层面的多线程基础
2.1 进程与线程
2.1.1 介绍
进程官方定义:进程是操作系统**进行资源分配和调度的最小单位**,是一个正在运行的程序实例。
一个可执行文件(比如你的a.out、.exe程序),当双击 / 命令行运行后,操作系统会为它分配独立的内存空间、CPU 时间片、文件句柄、网络端口等系统资源,这个运行中的程序实体,就是进程。
线程官方定义:线程是进程的一个执行单元,是操作系统进行CPU 调度和执行的最小单位,也被称为轻量级进程。
要理解多线程,首先得搞懂进程和线程的本质差异 —— 你可以把它们想象成 “工厂” 和 “工人” 的关系
进程:一家独立的工厂,有自己的厂房(内存空间)、设备(资源)、营业执照(PID),工厂之间完全隔离;
开新工厂成本高(需重新建厂房、买设备)。
线程:工厂里的工人 ,共享工厂的所有资源(厂房、设备),多个工人可同时干活;招新工人成本低(无需新建厂房,直接分配任务)。
具体例子:
- 你打开的微信是一个进程:它占用独立的内存(存聊天记录、界面数据),拥有 CPU、网络、磁盘等资源的使用权限;
- 微信里 “接收消息”“刷新朋友圈”“播放语音” 这些同时进行的操作,就是微信进程里的多个线程—— 它们共享微信的内存(比如都能访问你的好友列表),不用重复申请资源。
2.1.2 核心区别
| 进程(Process) | 线程(Thread) | 关键说明 | |
|---|---|---|---|
| 定义 | 操作系统分配资源的基本单位 | 操作系统调度执行的基本单位 | 进程是 “资源容器”,线程是 “容器里的执行流”;操作系统给进程分配内存、CPU 时间片等资源,线程负责实际执行代码。 |
| 内存空间 | 独立的地址空间(代码段、数据段、堆、栈);进程间内存完全隔离(默认无法直接访问)。 | 共享所属进程的全部内存空间(代码、数据、堆);仅拥有独立的栈空间(存储局部变量、函数调用信息)。 | 例:进程 A 的线程无法直接访问进程 B 的内存(需通过 IPC);同一进程的线程可直接读写同一块堆内存(这也是多线程数据竞争的根源)。 |
| 资源占用 | 占用资源多(内存、文件句柄、网络端口等);创建 / 销毁 / 切换成本高。 | 几乎不占用额外资源(仅需栈空间、线程 ID);创建 / 销毁 / 切换成本极低(是进程的 1/100~1/1000)。 | 进程切换:需保存整个进程的内存映射、寄存器状态等;线程切换:仅需保存线程的栈指针、寄存器,无需动进程资源。 |
| 独立性 | 高度独立:一个进程崩溃(如闪退),不会影响其他进程;进程间无默认共享数据。 | 高度依赖:一个线程崩溃(如空指针),会导致整个进程崩溃;线程间默认共享进程资源(易出并发问题)。 | 例:浏览器的一个标签页(进程)崩溃,其他标签页仍能用;微信的 “语音线程” 崩溃,整个微信都会闪退。 |
| 标识(ID) | 进程 ID(PID):全局唯一,操作系统统一管理。 | 线程 ID(TID):仅在所属进程内唯一;部分系统(如 Linux)TID 全局可见,但核心管理归属进程。 | 你在任务管理器里看到的 “PID” 是进程 ID,线程 ID 需通过工具(如 Process Explorer)查看。 |
| 通信方式 | 复杂且低效:需通过 IPC(管道、消息队列、共享内存、套接字等)。 | 简单且高效:直接读写共享内存(全局变量、堆变量);也可通过进程内的同步工具(互斥锁、条件变量)。 | 例:两个进程通信需写代码实现 “共享内存映射”;同一进程的线程只需定义一个全局变量,所有线程都能访问。 |
| 调度与执行 | 调度粒度粗:操作系统以进程为单位分配 CPU 时间片,再由进程分配给内部线程。 | 调度粒度细:操作系统直接调度线程,多个线程可在多个 CPU 核心上并行执行。 | 多进程:CPU 核心可能分配给不同进程;多线程:同一进程的多个线程可同时占用多个 CPU 核心(真正并行)。 |
| 创建方式 | 由操作系统创建(如 fork ()、CreateProcess ());需复制父进程的资源(或全新分配)。 | 由进程创建(如 pthread_create ()、std::thread);仅需分配线程栈和 TID,共享进程资源。 | 新建进程的时间:毫秒级;新建线程的时间:微秒级。 |
2.1.3 避坑点
-
线程共享资源是优势,也是最大的风险
优势:线程间通信成本极低,无需复杂的IPC,合适频繁数据交互的场景(如微信同时处理消息);
风险:多个线程之间共享资源(如全局变量和堆内存),会导致数据竞争(解决:互斥锁,原子操作)。
-
线程的“独立栈”和“共享堆”
新手常误认为线程的所有数据都是共享的,但实际上:
-
栈空间:每个线程有独立的栈(默认几 MB),栈上的局部变量是线程私有的,不会有数据竞争;
例:线程 A 的函数里定义
int a = 10;,线程 B 的函数里也定义int a = 20;,两者互不影响。 -
堆空间 / 全局变量:进程内所有线程共享,这是需要同步的核心区域;
例:进程里定义
int g_num = 0;,线程 A 执行g_num++,线程 B 也执行g_num++,可能导致 g_num 最终不是 2(数据竞争)。
-
-
并行和并发的区别
并发:多个任务 “交替执行”(如单核 CPU 上,进程 / 线程轮流占用 CPU,宏观看起来同时运行);
并行:多个任务 “同时执行”(如多核 CPU 上,多个线程分别在不同核心上运行);
-
线程切换的 “隐形开销”
虽然线程切换比进程快,但频繁切换(如数千个线程同时运行)仍会有开销:
-
操作系统需要保存 / 恢复线程的寄存器、栈指针等状态;
-
核心数有限时,过多线程会导致 “上下文切换风暴”,反而降低性能;
→ **解决:**CPU 密集型任务的线程数≈CPU 核心数;IO 密集型任务可适当增加线程数,但不宜过多。
-
-
进程和线程的 终止关系
- 进程是 “父”,线程是 “子”:进程退出时,操作系统会强制终止其所有线程;
- 线程无法脱离进程存在:线程必须属于某个进程,没有 “独立的线程”;
- 父进程退出后,子进程可能变成 “僵尸进程”(需手动回收),而线程退出后资源会被进程自动回收(除非是分离线程)。
2.2 线程的生命周期
2.2.1 介绍
线程完整的生命周期包括:新建、就绪、运行、阻塞、终止。

**新建态:**通过代码 / 系统调用,成功创建了线程对象,但是线程还没有被操作系统「纳入调度队列」,线程此时是「空壳」,不会执行任何代码。
**就绪态:**线程已经被操作系统「纳入调度队列」,线程具备了所有运行的条件,只差 CPU 的时间片。
运行态: 线程从「就绪队列」中被操作系统选中,成功抢到了 CPU 的时间片,开始执行线程的核心代码逻辑,这是线程生命周期中「唯一执行代码」的状态。
阻塞态: 线程在运行过程中,因为遇到了「无法继续执行」的条件,主动向操作系统申请「暂停执行」,操作系统会把这个线程从「运行态」切换到「阻塞态」,并立刻让出 CPU 时间片,分配给其他就绪态的线程。
注意: 阻塞态的线程,会被移出「就绪队列」,失去排队抢时间片的资格!哪怕 CPU 空闲,阻塞的线程也不会被执行。
可能的阻塞:
1.主动休眠:C++ 代码调用std::this_thread::sleep_for()/sleep_until()
2.主动让步:C++ 代码调用std::this_thread::yield(),线程主动让出 CPU,回到就绪态(yield 是「放弃本次时间片」,不是阻塞,是运行→就绪,不是运行→阻塞);)
3.等待锁资源:线程执行到std::mutex::lock()时,发现锁已经被其他线程占用,这个线程会进入阻塞态,直到锁被释放;
4.等待条件变量:线程执行到std::condition_variable::wait()时,会释放锁 + 进入阻塞态,直到被notify_one()/notify_all()唤醒;
5.等待 IO 操作:线程执行磁盘读写、网络请求、控制台输入等,因为 IO 速度远慢于 CPU,线程会阻塞直到 IO 完成;
6.等待其他线程:C++ 主线程调用t.join()时,主线程会进入阻塞态,直到子线程执行完毕。
终止态: 线程的核心代码逻辑执行完毕,或者线程被主动终止 / 异常终止,线程的生命周期彻底结束,进入终止态,这是线程的「最终状态」,状态不可逆,也无法恢复。
2.2.2 补充点
-
C++ 的 join () 和 detach ()
std::thread::join () - 主线程「阻塞等待 + 回收子线程」:调用
join()的主线程,会立刻从「运行态」→「阻塞态」,直到子线程执行完毕进入「终止态」;操作系统会自动回收子线程的所有资源,子线程不会变成僵尸线程; std::thread::detach () - 主线程「分离子线程,不回收」:调用
detach()后,子线程和主线程彻底脱离关系,子线程的生命周期不再受主线程控制;之后没有任何线程回收它的资源,会变成「僵尸线程」,僵尸线程会占用系统的线程 ID、内核资源,长期运行会导致系统资源耗尽,程序崩溃; -
阻塞的核心价值
阻塞和核心价值是不浪费 CPU,所有需要「等待某个条件满足」的场景,一定要用阻塞(sleep/wait/lock),绝对不要用忙等。
-
CPU 的「时间片」机制
现代操作系统都是多任务操作系统,哪怕你的电脑是单核 CPU,也能同时开微信、浏览器、IDE,核心原理就是:CPU 会给每个线程分配「时间片」(极短,一般几毫秒)。
2.3 线程的上下文切换
2.3.1 介绍
线程上下文切换 :操作系统内核,在暂停一个正在运行的线程、调度另一个就绪态线程占用 CPU的过程中,所执行的「保存当前线程上下文 → 加载新线程上下文」的完整操作**,是理解多线程性能、多线程开销、多线程调优的根基。**
首先:线程上下文切换是「有开销的」,这是多线程的最大代价之一,我们写 C++ 多线程代码的核心优化思路,本质就是「减少不必要的上下文切换」。
2.3.2 理解上下文切换的基础
-
CPU 的「单核串行、多核并行」本质:
CPU 的核心执行逻辑是:一个 CPU 核心,在同一个时刻,只能执行「一个线程」的代码。
- 单核 CPU:所有线程「轮流执行」,靠上下文切换实现「宏观并发」(看起来同时运行),微观上还是串行;
- 多核 CPU:多个线程可以「同时执行」(并行),每个核心跑一个线程,但核心数永远有限(比如 8 核 16 线程),当线程数>核心数时,依然需要上下文切换。
-
CPU 的「时间片调度机制」 上面介绍过
-
线程的「运行依赖 CPU 寄存器」
CPU 执行线程代码时,不会把线程的所有数据都读到内存里,而是会把线程的「核心运行数据」存到 CPU 的「寄存器」中(寄存器是 CPU 内部的高速存储,速度比内存快百倍)。
比如:线程执行到哪一行代码、当前的变量值、函数调用的栈地址、堆内存的指针等,全部存在寄存器里。
2.3.3 什么是线程上下文
线程上下文 = 线程的「运行状态全集」,就是一个线程能「随时暂停、随时恢复执行」所需要的所有核心数据。这些数据是线程运行的「全部家底」,缺了任何一点,线程暂停后就再也恢复不了执行。
完整的线程的上下文分为==「线程私有」和「CPU 专属」==,全部由操作系统内核维护,这两部分合起来,就是线程的完整上下文:
1. 线程自身的私有数据(线程创建时就分配,和线程绑定)
-
线程的独立栈空间:存储线程的局部变量、函数调用栈、返回地址等;
-
线程的线程 ID(TID)、优先级、所属进程 ID(PID);
-
线程的状态标记:就绪 / 运行 / 阻塞 / 终止。
2. CPU 寄存器中的核心数据(线程运行时临时存入,CPU 专属)
这是上下文的核心核心,也是上下文切换的「主要开销来源」,CPU 执行哪个线程,寄存器里就存哪个线程的这些数据:
-
程序计数器(PC):记录线程当前执行到代码的哪一行(下一条要执行的指令地址);
-
栈指针寄存器:记录线程栈空间的当前位置;
-
通用寄存器:记录线程运行时的变量值、运算结果等临时数据;
-
状态寄存器:记录线程的运行状态(比如是否执行了加法 / 减法、是否有异常)。
2.3.4 线程上下文切换的步骤
整个上下文切换的步骤是由操作系统内核自动完成的,程序员(包括 C++ 代码)完全无法干预,我们写的std::thread、sleep_for、yield等代码,只是「触发」上下文切换,而不是「执行」切换。下面是具体的步骤:

2.3.5 上下文切换触发的场景
场景1: 线程的时间片用完(被动切换)
线程正常运行,时间片耗尽,操作系统强制切换,线程从「运行态」→「就绪态」,这是最频繁的上下文切换场景,无任何代码触发,纯操作系统调度。
场景2:线程主动放弃CPU(主动切换)
线程在运行中,调用了 C++ 的相关函数,主动让出 CPU,触发上下文切换,例如函数:
std::this_thread::sleep_for(xx):线程主动休眠 → 「运行态」→「阻塞态」,触发切换;std::this_thread::yield():线程主动让步 → 「运行态」→「就绪态」,触发切换。
场景3:线程等待资源/条件(主动切换,C++ 多线程核心)
C++ 多线程最最最常用的场景,几乎所有带锁、带条件变量的代码都会触发,也是上下文切换的核心场景,例如下面的场景:
- 线程执行
std::mutex::lock(),发现锁被占用 → 线程进入「阻塞态」,触发切换; - 线程执行
std::condition_variable::wait()→ 释放锁 + 进入「阻塞态」,触发切换; - 线程等待 IO 操作(磁盘读写、网络请求)、等待控制台输入 → 进入「阻塞态」,触发切换;
- 主线程执行
t.join()→ 主线程进入「阻塞态」,触发切换。
场景4:线程优先级抢占(被动切换)
操作系统会给线程分配优先级,高优先级的线程会抢占低优先级线程的 CPU 时间片。
场景5:线程正常终止/异常终止
2.3.6 线程和进程上下文切换的区别
| 对比 | 线程上下文切换 | 进程上下文切换 | 核心差异总结 |
|---|---|---|---|
| 切换的核心内容 | 只切换「线程私有上下文」:线程栈、寄存器数据、线程 ID、状态;不切换进程的资源(内存、文件句柄、堆等)。 | 不仅切换「进程内线程的上下文」,还要切换「进程的全部资源」:进程的地址空间、内存映射表、文件句柄、堆内存、PID 等。 | 线程切换是「小范围切换」,进程切换是「大范围切换」。 |
| 切换开销 | 极小(微秒级)只需要保存 / 加载线程的寄存器和栈,无额外开销。 | 极大(毫秒级)需要保存 / 加载进程的所有资源,还要刷新 CPU 的内存缓存,开销是线程的 100~1000 倍 | 这是「为什么多线程比多进程更高效」的核心原因! |
| 切换频率 | 极高(每秒上千次),多线程程序的常态。 | 极低(每秒几次到几十次),多进程程序的切换频率远低于线程。 | 线程是轻量级的执行单元,就是因为切换成本低。 |
| C++ 中对应场景 | 所有std::thread的切换都是线程上下文切换。 |
比如你同时开了 VSCode 和微信,这两个进程的切换就是进程上下文切换。 | C++ 多线程开发,本质就是用「低开销的线程切换」实现并发。 |
还拿工人和工厂的例子:
- 线程上下文切换 = 同一个工厂里,换一个工人干活:工厂的厂房、设备、原材料都不变,只需要把上一个工人的工具、草稿纸收起来,给下一个工人换上他的工具、草稿纸,立刻就能开工;
- 进程上下文切换 = 换一个工厂干活:需要把上一个工厂的厂房、设备、原材料全部封存,再打开新工厂的厂房、设备、原材料,才能开工,耗时巨大。
2.3.7 线程上下文切换的开销
通过上文我们知道线程的上下文切换会带有开销,其实所有的开销都是操作系统内核做的事情,具体分为直切开销和简介开销:
1 直接开销
就是操作系统执行保存上下文,加载上下文的消耗,这是固定的,无法避免的;例如,内核需要读写内存(把寄存器的数据写入TCB,从TCB读取数据到寄存器中),修改线程状态标记,加入到就绪队列中。
2 间接开销
简介开销远大于直接开销,也是上下文切换最大的开销,主要有以下两个方面:
-
CPU寄存器缓存失效
CPU的寄存器是高速存储,线程运行中数据都保存在寄存器里,线程上下文切换后,寄存器中的内容全部被替换为新线程的数据,旧线程的数据全部作废。
-
CPU多级缓存(L1/L2/L3)失效
CPU 为了提速,会把线程常用的内存数据缓存到 CPU 的 L1/L2/L3 缓存中
2.3.8 注意点
-
线程数量不是越多越好,而是恰到好处。
当线程数远大于CPU核心数时候,操作系统会疯狂的进行上下文切换。例如8 核 CPU,开了 100 个线程
情况①:CPU密集型任务
例如,排序,计算,处理逻辑,这类任务的特点是:线程一直占用CPU,几乎不阻塞,不等待资源。
最优线程数 = CPU核心数(多一两也可以)
情况②:IO密集型任务
例如,文件的读写,网络的请求,数据操作,这类任务的特点是:线程大多时候都在堵塞等待(如磁盘的读写,等网络响应),真正用到CPU的时间确极少。
最优线程数 = CPU 核心数 × 2 到 CPU 核心数 × 5
-
线程的上下文切换,只保存栈的「指针地址」,而不是栈里的所有数据,栈空间的大小(比如默认 2MB),对上下文切换的开销没有任何影响。
2.4 并发优势与风险
2.4.1 介绍
首先,并发,在多线程的场景下,特指同一进程内的多线程并发,并发是一个双刃剑,用好,程序的性能会提升数倍(这是c++开发所追求的),但是用不好,程序会出现各种诡异的 BUG、崩溃、性能暴跌,而且这类问题极难排查!
并发:多个任务「交替执行」,在宏观上看起来同时运行,微观上是串行执行。
并行:多个任务「真正同时执行」,在宏观和微观上都是同时运行。
无论并发还是并行,我们统称「并发编程」,下文所有提到的并发,都包含这两种情况。
2.4.2 优势
-
最大化利用 CPU 资源
-
彻底解决「耗时任务导致程序卡死」的问题,提升程序的响应性
多线程程序是「并发执行」的:我们可以把「耗时任务」放到后台线程中执行,主线程(UI 线程 / 业务主线程)继续运行,处理用户操作、界面刷新、请求响应,程序永远不会卡死,用户体验极好。
场景 1:桌面 GUI 程序(Qt/C++ MFC)
比如你写一个 C++ 桌面程序,需要「读取一个 10GB 的大文件」,如果用单线程:点击「读取文件」后,程序界面立刻卡死,鼠标点不动、按钮点不了,直到文件读取完成才恢复;如果用多线程:把「读取文件」的逻辑放到一个后台线程中执行,主线程继续刷新界面、响应用户操作,用户可以正常点击按钮、拖动窗口,文件读取完成后,后台线程通知主线程更新数据,程序全程流畅无卡顿。
场景 2:服务端程序(TCP/UDP/Http 服务)
比如你写一个 C++ 的网络服务端,需要处理客户端的请求,如果用单线程:同一时间只能处理 1 个客户端请求,其他客户端只能排队等待,服务端的响应速度极慢,并发量极低;
-
线程间通信成本极低,数据共享无需复杂操作
-
任务解耦,让代码逻辑更清晰、更易维护
多线程程序的代码逻辑是「模块化」:我们可以把不同的功能拆分成不同的线程,每个线程只负责做一件事,线程之间通过简单的方式通信,代码职责清晰,耦合度极低。
-
线程的创建 / 销毁 / 切换成本极低,支持海量并发
2.4.3 劣势
-
数据竞争
线程的核心是统一进程内的线程共享全局变量,静态变量,堆内存,多个线程在没有线程保护的情况下,同时对同一块共享数据进行读写操作,导致结果不符。
(程序员就是要写代码啊,上面的字都快写吐了,哈哈,来看代码吧)
#include<iostream> #include<thread> int global_num = 0; void add() { for (int i = 0;i < 10000;i++) { global_num++; } } int main() { std::thread thread01(add); std::thread thread02(add); thread01.join(); thread02.join(); std::cout << global_num << std::endl; return 0; }执行结果:

每次执行结果可能不一样(你的电脑如果性能好甚至肯num=20000),但是上面可以表现出问题,主要原因是:
global_num++这个看似简单的操作,在 CPU 层面是3 个独立的非原子操作:-
从内存中读取
global_num的值到 CPU 寄存器; -
寄存器中的值 + 1;
-
把寄存器中的值写回内存的
global_num。 多线程执行时,这 3 步会被「上下文切换」打断,比如:线程 1 执行完步骤 1 和 2,还没写回内存,线程 2 就开始执行步骤 1,读取到了旧值,最终导致自增次数丢失,结果偏小。
解决办法:
for (int i = 0;i < 10000;i++) { std::lock_guard<std::mutex> lock(mtx_); global_num++; }修改上面的for循环内容即可
-
-
死锁
多个线程互相持有对方需要的锁,同时又等待对方释放锁,导致所有线程都进入「永久阻塞态」,再也无法继续执行,整个程序卡死。
死锁的 4 个必要条件(必背,缺一不可,打破任何一个条件就不会死锁):
-
互斥:锁的资源是独占的,同一时间只能被一个线程持有;
-
持有并等待:线程持有一个锁,同时又等待另一个锁;
-
不可抢占:锁只能被持有它的线程主动释放,不能被其他线程抢占;
-
循环等待:线程 A 等待线程 B 的锁,线程 B 等待线程 A 的锁,形成闭环。
#include <iostream> #include <thread> #include <mutex> #include <chrono> std::mutex mtx1; // 锁1 std::mutex mtx2; // 锁2 // 线程1:先加锁1,再加锁2 void func1() { mtx1.lock(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 故意延迟,让线程2先加锁2 mtx2.lock(); // 卡死在这里:线程2已经持有锁2,等待锁1 std::cout << "线程1执行完毕" << std::endl; mtx2.unlock(); mtx1.unlock(); } // 线程2:先加锁2,再加锁1 void func2() { mtx2.lock(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 故意延迟,让线程1先加锁1 mtx1.lock(); // 卡死在这里:线程1已经持有锁1,等待锁2 cout << "线程2执行完毕" << endl; mtx1.unlock(); mtx2.unlock(); } int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); // 永久阻塞,程序卡死 t2.join(); return 0; }运行结果:
没有结果卡死 哈哈哈
-
-
线程的竞态终止与僵尸线程
竞态终止:
主线程和子线程的执行顺序是「不确定的」,如果主线程先于子线程执行完毕并退出,而子线程还在运行,会导致两种严重后果:
- 子线程如果访问了「主线程的局部变量」,这些变量已经被销毁,子线程会访问野指针,直接崩溃;
- 主线程退出后,整个进程会被操作系统终止,所有子线程都会被强制杀死,业务逻辑执行失败。
僵尸进程:
线程执行完毕进入「终止态」后,操作系统需要回收线程的内核资源(线程 ID、栈空间、寄存器数据);如果主线程没有调用t.join() 回收这些资源,线程就会变成「僵尸线程」,持续占用系统资源。
-
线程的「异常终止」导致整个进程崩溃
线程是「进程的一部分」,线程没有独立的地址空间,所有线程共享进程的内存和资源;进程内的任意一个线程,如果发生「未捕获的异常」或「内存错误」(空指针、内存越界、数组越界),会直接触发整个进程的崩溃!
错误例子:
void func() { int* p = nullptr; *p = 10; // 空指针访问,线程异常终止 } int main() { thread t(func); t.join(); cout << "主线程继续执行" << endl; // 永远不会执行,因为进程已经崩溃 return 0; }正确做法: 在所有线程的入口函数 / Lambda 中,用try-catch捕获「所有异常」
void func() { try { int* p = nullptr; *p = 10; // 空指针访问,抛出异常 } catch (...) { // 捕获所有异常 cout << "线程内部捕获到异常,线程安全退出" << endl; return; } } int main() { thread t(func); t.join(); cout << "主线程继续执行" << endl; // 正常执行,进程不会崩溃 return 0; } -
过度并发导致的上下文切换风暴,性能暴跌
更多推荐



所有评论(0)