一、基本概念区分
线程是进程的子集,一个进程可包含多个线程;进程是资源分配单位,线程是调度执行单位。
并发是 "交替执行"(单 / 多核均可),并行是 "同时执行"(必须多核)。并行是并发的一种特殊情况,并发包含并行。
1.1 进程(Process)
一个正在运行的程序,是操作系统进行资源分配(如内存、文件句柄、CPU 时间片等)的基本单位。
特点:
- 每个进程拥有独立的内存空间、数据栈和系统资源,进程间相互隔离,互不干扰。
- 进程间通信(IPC)需要通过特定机制(如管道、消息队列、共享内存等),开销较大。
- 进程切换时,操作系统需要保存和恢复整个进程的状态,开销较高。
例子:打开一个浏览器是一个进程,同时打开的 Word 是另一个独立进程。
1.2 线程(Thread)
线程是进程内的一个执行单元,是 CPU 调度的基本单位。一个进程可以包含多个线程,它们共享进程的内存空间和资源。
特点:
- 线程共享所属进程的内存、文件句柄等资源,线程间通信通过共享内存即可,开销小。
- 线程切换只需保存线程的局部变量、程序计数器等少量状态,开销远小于进程切换。
- 线程依赖于进程存在,进程终止后,其所有线程也会终止。
例子:浏览器进程中,一个线程负责渲染页面,另一个线程负责下载文件,它们共享浏览器的内存资源。
1.3并发(Concurrency)
指多个任务在同一时间段内交替执行(宏观上看起来同时进行,微观上可能是串行的)
开起来同时执行,实际上是快速切换
核心 :通过任务切换(如 CPU 时间片轮转)实现 "同时" 处理多个任务,适用于单 CPU例子:
- 单 CPU 电脑上,同时听歌、聊微信、写文档:CPU 快速在三个进程 / 线程间切换,让用户感觉它们在同时运行。
- 本质是 "交替执行",不是真正的同时。
1.4并行(Parallelism)
指多 个任务在同一时刻真正同时执行 ,需要多 CPU 核心或多核处理器支持。
核心:多个任务在不同的 CPU 核心上并行处理,微观上是真正的同时进行。例子:
- 多核心 CPU 中,一个核心运行音乐播放线程,另一个核心运行微信聊天线程,两者真正同时执行。
- 只有在多 CPU / 多核环境下才能实现并行。
二、创建线程
在 Visual Studio 中使用 C++ 创建线程,可以使用 C++11 标准引入的
std::thread库过程:
- 定义线程函数:可以是无参或带参函数
- 创建线程对象:
std::thread t(函数名, 函数参数...)- 使用
join()方法:让主线程等待子线程完成,避免主线程提前退出,子线程可能还没完成,造成程序崩溃join()等待+资源回收+置为非可结合状态
std::thread类的一个成员函数,用于阻塞当前线程(通常是主线程),等待被调用的子线程执行完毕后,再继续执行主线程剩下的代码,当执行结束后回收进程资源例子:假设你是主线程,你派了一个子线程去 "打印 1000 行日志",如果调用
t.join():
- 主线程会停在
t.join()这一行,什么都不做,直到子线程把 1000 行日志打印完;- 子线程执行完毕后,主线程才会继续执行
join()后面的代码;- 如果子线程已经执行完了,
join()会立即返回,不会阻塞注意:
(1)一个对象只可以调用一次join函数,调用一次后线程变为非可结合状态,再次调用会触发
std::terminate()崩溃非可结合状态-----调用joinable函数时返回false
(2)即使子线程先执行完,
join()仍需调用 ------ 它的核心作用还有 "将线程对象置为非可结合状态",否则析构时仍会崩溃。detach()(分离线程),调用后主线程不再等待子线程,子线程会在后台独立运行,直到完成。分离后的线程无法再通过std::thread对象控制(子线程可能执行完了,也可能没执行玩)
示例一:创建线程
线程对象之间只能移动赋值,移动构造
不可以拷贝构造、赋值
C++标准库thread中,将拷贝构造函数=delete
将=运算符=delete
cppthread(const thread&)=delete; thread& operator=(const thread&)=delete;*******为什么不可拷贝构造?
(1)浅拷贝:两个线程对象会共享同一块内存空间,导致一份资源被重复释放两次
(2)深拷贝:线程的核心是 "正在执行的函数(及函数上下文,如栈、寄存器状态)",这些是动态的、运行时的资源,无法通过简单的 "内存拷贝" 复制(例如,线程执行到函数的第 5 行,拷贝后新线程不可能直接从第 5 行继续执行);
cppvoid func(int ax) { for (int i = 0; i < 10; i++) { printf("%p----%d", &ax, ax); printf("\n"); } } int main() { //线程函数 函数参数 2个线程公用一个线程函数,不存在竞争关系,各自单独使用创建形参ax,故有多少个线程,就有多少份形参 thread tha(func, 10);//创建线程tha thread thb(func, 20);//线程thbd //不可以通过拷贝构造创建线程 thread thc(tha);//error //移动构造可创建线程 thread thd (std::move(tha)); thd.join(); thb.join(); }这段代码示例中包含3个线程:主线程、线程tha、线程thb
main函数执行时,创建线程,系统为线程分配各自的内核(Windows下1M,Linux下10M),每个线程在自己分配的内核空间中包含自己的头部信息、实参等,所有,有几个线程就有几份形参(代码中,打印形参地址时,会有2个值)
如果线程不调用join(),程序崩溃。原因是:
std::thread对象有一个关键特性:当std::thread对象的析构函数被调用时,如果检测到线程既没有被join()(等待完成),也没有被detach()(分离),析构函数会调用std::terminate()终止整个程序。示例二:值传递(效果最好)
无竞争,线程独立
cppvoid func(Int ax) { for (int i = 0; i < 10; i++) { printf("%p----%d", &ax, ax); printf("\n"); } } int main() { Int x(10); thread tha(func, x); ++x;//11 thread thb(func, x); tha.join(); thb.join(); }结果分析:
(1)构造函数创建对象x=10
(2)创建线程tha,把x参数传递,调用拷贝构造函数,在主线程的临时内存中复制
x(值 10),作为线程参数的中间副本。再调用移动构造函数,把临时内存的x(10)移动到子线程tha的栈区,作为形参ax
(3)主线程x+1 (线程独立性 :两个子线程的
ax是独立的副本(地址不同),值分别为创建线程时x的快照(10 和 11),与主线程后续修改的x无关)(4)创建线程thb,拷贝构造实参到主线程的临时区,再调用移动构造,移动该副本到子线程thb的栈区
优势:每个线程的参数值修改时,不影响主线程x的值
劣势:当有多个数据时,会大量调动拷贝构造和移动构造
示例三:指针传递
无拷贝构造,移动构造,无副本,有竞争,线程不独立,主线程的
x和两个子线程的ax指针指向**同一块内存,**主线程值修改会影响子进程形参引发问题:
(1)线程安全问题,当一个线程正在+1操作时,另一个线程刚好再读数据,导致程序崩溃
(2)空悬指针 解决:改变线程函数的指针所指对象的生命周期
解决:加互斥锁、改为值传递
线程安全代码示例:
cppvoid func(Int *ax) { assert(ax != nullptr); for (int i = 0; i < 10; ++i) { cout<< *ax << endl; *ax++; } } int main() { Int x(0); thread tha(func, &x); thread thb(func, &x); tha.join(); thb.join(); }空悬指针示例:
cppvoid func(Int *ax) { assert(ax != nullptr); for (int i = 0; i < 10; ++i) { cout<< *ax << endl; } } int main() { thread tha; { Int x(0); tha = thread(func, &x);//块作用域结束,指针ax所指地址空间释放,称为空悬指针 } tha.join(); return 0; }示例四:引用传递
竞争,线程不独立
如果存在块作用域,效果和指针相同,要注意引用对象的生命周期
cppvoid func(Int &ax) { for (int i = 0; i < 10; ++i) { cout << ax; ++ax; } } int main() { Int x(0); thread tha(func, std::ref(x)); tha.join(); return 0; }
std::ref(x)的核心作用是将变量包装为引用包装器 ,解决std::thread/std::bind等场景中 "参数默认拷贝" 的问题,让函数能真正操作原变量。std::ref对应非 const 引用,std::cref对应 const 引用,需根据函数参数类型选择。- 严禁用
std::ref包装临时变量,否则会产生悬空引用,导致程序崩溃。2.1 jthread
jthread 是thread的升级版,C++20引入
特点:无需手动掉用join / detch,子线程析构时会自动调用
三、线程相关函数
3.1 joinable
检查线程是否可合并(是否可以终止线程并回收资源),即是否可能在并行上下文中运行
返回
true(1):线程对象关联了一个未被join()或detach()处理的底层线程(可能正在运行、已就绪或已终止但未被回收)。返回
false(0):线程对象未关联任何有效底层线程(如默认构造的空线程、已用join()/detach()的线程、被移动构造 / 赋值后 "掏空" 的线程)。
对一个不可连接 的线程对象调用
join()或detach(),会导致未定义行为(通常表现为程序崩溃)。因此,在调用join()或detach()前,通常需要用joinable()检查线程状态,确保操作安全
cppint main() { thread tha; cout << tha.joinable()<<endl;//空对象返回false 0 thread thb(func, 10); cout << thb.joinable() << endl;//1 thb.join(); cout << thb.joinable() << endl;//以调用join的对象,返回false 0 return 0; }int main() { thread tha; cout << tha.joinable()<<endl;//空对象返回false 0 线程对象此时为空 thread thb(func, 10); cout << thb.joinable() << endl;//1 thb.join(); cout << thb.joinable() << endl;//已调用join的对象,返回false 0 return 0; }3.2 get_id
返回线程的 id
四、mutex
mutex头文件主要声明了与互斥量(mutex)相关的类。
mutex提供了4种互斥类型:
std::mutex 最基本的 Mutex 类。
std::recursive_mutex 递归 Mutex 类。
std::time_mutex 定时 Mutex 类。
std::recursive_timed_mutex 定时递归 Mutex 类。
4.1 lock与unlock
lock:上锁
unlock:解锁
死锁 :是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
同一个mutex变量上锁之后,一个时间段内,只允许一个线程访问它。例如:
cpp//全局锁 std::mutex mylock; void func(char ch) { mylock.lock();//上锁 for (int i = 0; i < 10; ++i) { for (int j = 0; j < 10; j++) { printf("%c", ch); } printf("\n"); } printf("\n"); mylock.unlock();//解锁 } int main() { thread tharr[5];//线程数组 for (int i = 0; i < 5; i++) { tharr[i] = thread(func, 'A' + i);//数组赋值 } for (int i = 0; i < 5; i++) { tharr[i].join(); } return 0; }4.2 lock_guard轻量级、简单场景
使用C++11的RAll机制(获取资源即初始化),自动管理锁的生命周期,避免死锁
lock_guard的特点:
(1)、创建即加锁(自动调用mutex::lock),作用域结束自动析构并解锁(自动调用mutex::unlock),无需手动解锁
(2)、不能中途解锁 ,必须等作用域结束才解锁
(3)、不能拷贝、移动(避免意外解锁)
cpp//全局锁 std::mutex mylock; void func(char ch) { //构造时自动加锁 lock_guard<mutex>lock(mylock);//模板类,lock_guard是一个类,mutex是模板参数 for (int i = 0; i < 10; ++i) { for (int j = 0; j < 10; j++) { printf("%c", ch); } printf("\n"); } printf("\n"); //超出作用域时自动解锁 } int main() { thread tharr[5]; for (int i = 0; i < 5; i++) { tharr[i] = thread(func, 'A' + i); } for (int i = 0; i < 5; i++) { tharr[i].join(); } return 0; }4.3 unique_lock
unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。
unique_lock的特点:
**创建时可以不锁定(通过指定第二个参数为std::defer_lock延迟加锁),而在需要时再锁定
**可以随时手动加锁解锁
**作用域规则同 lock_grard,析构时自动释放锁
**不可复制,可移动
**条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
所有 lock_guard 能够做到的事情,都可以使用 unique_lock 做到,反之则不然。那么何时使lock_guard呢?需要使用锁的时候,首先考虑使用 lock_guard,因为lock_guard是最简单的锁。
cppvoid func(char ch) { std::unique_lock<std::mutex> guard(mylock, std::defer_lock); // 构造时不加锁 // 非临界区代码... guard.lock(); // 手动加锁 // 临界区... guard.unlock(); // 手动解锁 // 非临界区... guard.lock(); // 可再次加锁 // 临界区... } // 析构时如果锁是加锁状态,自动解锁
五、睡眠相关函数
睡眠函数:让当前线程暂停指定的执行时间(放弃CPU时间片)
5.1 std::this_thread::sleep_for
C++11 的标准库,引入头文件<thread>
5.2 支持的时间单位(chrono 库)
| 时间单位 | 含义 | 示例 |
|---|---|---|
std::chrono::milliseconds(n) |
毫秒 | sleep_for(100ms) → 睡眠 100 毫秒 |
std::chrono::seconds(n) |
秒 | sleep_for(5s) → 睡眠 5 秒 |
std::chrono::microseconds(n) |
微秒 | sleep_for(500us) → 睡眠 500 微秒 |
std::chrono::nanoseconds(n) |
纳秒 | sleep_for(1000ns) → 睡眠 1000 纳秒 |
std::chrono::minutes(n) |
分钟 | sleep_for(2min) → 睡眠 2 分钟 |
代码示例:
cpp
#include<iostream>
#include<thread>
#include<chrono>//时间单位:毫秒、秒等
using namespace std;
void threadFuna()
{
cout << "线程开始,即将睡眠3秒" << endl;
std::this_thread::sleep_for(std::chrono::seconds(3));//睡眠3秒
cout << "睡眠结束,继续运行" << endl;
}
int main()
{
std::thread tha(threadFuna);//创建thread类的对象
tha.join();
return 0;
}
六、 condition_variable
条件变量用于线程间的通信,一个线程可以等待某个条件变为真,而另一个线程可以通知等待的线程条件已经满足。
cpp
#include <stdio.h>
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>//头文件
using namespace std;
std::mutex mutex_;
std::condition_variable cv;//条件变量
bool is_ok = false;//等待的条件,为true时唤醒
//线程A 等待
void wait_thread()
{
//unique_lock只是锁的一个管理者,实现自动上锁解锁,赋值的mutex_才是真正的锁
std::unique_lock<std::mutex> lock(mutex_);
//条件不满足,睡觉(此时自动解锁)
cv.wait(lock, [] {return is_ok; });//返回条件
cout << "我被叫醒" << endl;
}
//线程B 唤醒
void notify_thread()
{
std::this_thread::sleep_for(std::chrono::seconds(2));
std::lock_guard<mutex>lock(mutex_);
is_ok = true;//修改条件
cv.notify_one();//唤醒等待的线程
cout << "我来叫醒你" << endl;
//wait 需要中途 解锁 +睡眠+ 重锁 → 必须 unique_lock
//notify 只需要简单保护临界区 → lock_guard 足够
//lock_guard 轻量简单,unique_lock 灵活强大
}
int main()
{
std::thread tha(wait_thread);
std::thread thb(notify_thread);
tha.join();
thb.join();
return 0;
}
输出结果:
我来叫醒你
我被叫醒
