C++多线程

一、基本概念区分

线程是进程的子集,一个进程可包含多个线程;进程是资源分配单位,线程是调度执行单位。

并发是 "交替执行"(单 / 多核均可),并行是 "同时执行"(必须多核)。并行是并发的一种特殊情况,并发包含并行。

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

过程:

  1. 定义线程函数:可以是无参或带参函数
  2. 创建线程对象:std::thread t(函数名, 函数参数...)
  3. 使用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

cpp 复制代码
thread(const thread&)=delete;
thread& operator=(const thread&)=delete;

*******为什么不可拷贝构造?

(1)浅拷贝:两个线程对象会共享同一块内存空间,导致一份资源被重复释放两次

(2)深拷贝:线程的核心是 "正在执行的函数(及函数上下文,如栈、寄存器状态)",这些是动态的、运行时的资源,无法通过简单的 "内存拷贝" 复制(例如,线程执行到函数的第 5 行,拷贝后新线程不可能直接从第 5 行继续执行);

cpp 复制代码
void 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() 终止整个程序

示例二:值传递(效果最好)

无竞争,线程独立

cpp 复制代码
void 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)空悬指针 解决:改变线程函数的指针所指对象的生命周期

解决:加互斥锁、改为值传递

线程安全代码示例:

cpp 复制代码
void  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();
	
}

空悬指针示例:

cpp 复制代码
void  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;

}

示例四:引用传递

竞争,线程不独立

如果存在块作用域,效果和指针相同,要注意引用对象的生命周期

cpp 复制代码
void  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;
	
	
	
}
  1. std::ref(x) 的核心作用是将变量包装为引用包装器 ,解决 std::thread/std::bind 等场景中 "参数默认拷贝" 的问题,让函数能真正操作原变量。
  2. std::ref 对应非 const 引用,std::cref 对应 const 引用,需根据函数参数类型选择。
  3. 严禁用 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() 检查线程状态,确保操作安全

cpp 复制代码
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;
	
	
	
}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是最简单的锁。

cpp 复制代码
void 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;
}

输出结果:
我来叫醒你
我被叫醒
相关推荐
野生技术架构师4 小时前
金三银四面试总结篇,汇总 Java 面试突击班后的面试小册
java·面试·职场和发展
_深海凉_4 小时前
LeetCode热题100-寻找两个正序数组的中位数
算法·leetcode·职场和发展
ja哇5 小时前
大厂面试高频八股
java·面试·职场和发展
Advancer-7 小时前
第二次蓝桥杯总结(上)
java·算法·职场和发展·蓝桥杯
空中海7 小时前
Spring Cloud 专家级面试题库
spring·spring cloud·面试
weixin_426184977 小时前
系统设计面试009:设计 Facebook 新闻动态(News Feed)
面试
拾贰_C8 小时前
【OpenClaw | openai | QQ】 配置QQ qot机器人
运维·人工智能·ubuntu·面试·prompt
空中海8 小时前
Spring Boot 专家级面试题库
spring boot·后端·面试