C++多线程并发 基础入门教程

本文详细介绍了C++中的多线程编程基础,包括如何创建线程、线程同步(互斥量、死锁处理)、原子变量、条件变量与信号量的应用,以及promise和future的概念和使用。通过实例展示了如何避免资源竞争,解决线程间通信问题,以及如何通过std::async简化异步任务执行。此外,还提到了std::packaged_task用于任务封装的特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文为教程C++ 多线程并发 基础入门教程的学习笔记

1.基础知识

1.1 创建一个线程

std::thread thread1(函数名,函数的参数列表)

#include <iostream>
#include <thread>

void func(int a){
	...
	}
int main(){
	int a = 0;
	std::thread thread1(func,a);	//创建一个线程thread1并开始执行
	while(true);
	}

如果main()中不加while(true),则会报错,因为主线程执行完毕后会退出,而子线程还没有执行完毕,因此会报错。
解决方案

1.1.1 调用成员函数join()

该成员函数会将主线程阻塞住,直到子线程结束

#include <iostream>
#include <thread>

void func(int a){
	...
	}
int main(){
	int a = 0;
	std::thread thread1(func,a);	//创建一个线程thread1并开始执行
	thread1.join();	//阻塞主线程,直到子线程thread1执行完毕
	}
1.1.2.调用成员函数detach()

该成员函数会将主线程和子线程完全分开,不等待子线程执行完毕,直接执行下一条语句。

#include <iostream>
#include <thread>

void func(int a){
	...
	}
int main(){
	int a = 0;
	std::thread thread1(func,a);	//创建一个线程thread1并开始执行
	thread1.detach();	//将子线程和主线程完全分开
	}

尽量不要使用detach

1.2 常用成员

get_id():获取线程id
hardware_concurrency():返回当前硬件支持的最大并发数

1.3 在函数中获取执行该函数的线程信息

std::this_thread::get_id():获取执行当前函数的线程号

#include <iostream>
#include <thread>

void func(int a){
	std::cout<<std::this_thread::get_id()<<std::endl;//输出执行当前线程的线程号
	std::this_thread::sleep_for(std::chrono::microseconds(50));	//休眠50ms
	}
int main(){
	int a = 0;
	std::thread thread1(func,a);	//创建一个线程thread1并开始执行
	thread1.join();	//阻塞主线程,直到子线程thread1执行完毕
	}

2. 互斥量及死锁现象

2.1 基础知识

如果两个线程同时访问一个公共资源(如全局变量),便会导致该公共资源不安全,如:

#include <iostream>
#include <thread>

int globalVar = 0;
void task1{
	for (int i = 0; i<10000 ;i++){
		globalVar++;
		globalVar--;
		}
	}
int main(){
	std::thread t1(task1);
	std::thread t2(task1);
	
	t1.join();
	t2.join();
	std::cout<<globalVar<<std::endl;
	}

该程序输出的结果不会是0,因为两个线程都在访问globalVar。解决方法:使用互斥量

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int globalVar = 0;

void task1{
	for (int i = 0; i<10000 ;i++){
		mtx.lock();
		globalVar++;
		globalVar--;
		mtx.unlock();
		}
	}
int main(){
	std::thread t1(task1);
	std::thread t2(task1);
	
	t1.join();
	t2.join();
	std::cout<<globalVar<<std::endl;
	}

此时程序输出为0

2.2 死锁现象与解决方式

使用mutex可能会产生死锁现象,如
2.2.1.上锁顺序不同导致死锁
例程中两个线程都在等待,形成死锁。

void task1(){
	mtx1.lock();
	mtx2.lock();
	...
	mtx2.unlock();
	mtx1.unlock();
	}
void task2(){
	mtx2.lock();
	mtx1.lock();
	...
	mtx1.unlock();
	mtx2.unlock();
	}

解决方法

  1. 不同函数中上锁的时候,保持同样的上锁顺序
  2. 使用std::lock()代替多个锁上锁
void task1(){
	std::lock(mtx1,mtx2);
	...
	mtx1.unlock();
	mtx2.unlock();
	}

2.2.2 程序中途返回造成死锁
例程中程序提前返回,mtx1没有解锁

void task1(){
	mtx1.lock();
	if (1 == 1) return;
	mtx1.unlock();
	}

解决方法:使用标准库中的模板类std::lock_guard <std::mutex>或std::unique_lock <std::mutex>,这两个类会在构造的时候上锁,析构的时候解锁。
其中std::unique_lock <std::mutex>使用更灵活,该类中有多个成员函数,可以自由控制解锁,而不必等到析构时才能解锁。例:

void task1(){
	std::lock_guard <std::mutex> lock1(mtx1);
	if (1 == 1) return;
	std::cout<<"Have not return"<<std::endl;
	}
void task2(){
	std::unique_lock <std::mutex> lock2(mtx2);
	if (1 == 1) return;
	std::cout<<"Have not return"<<std::endl;
	lock2.unlock();
	std::cout<<"Have not return but have unlocked"<<std::endl;
	}

2.3 原子变量

原子变量能够不通过互斥量、锁的方式避免资源竞争
将可能产生竞争的全局变量定义为如下方式即可

#include <atomic>

std::atomic<int> globalVar = 0;

通过原子变量可以避免资源竞争,其具体实现方式依编译器而定,有的是编译器自动为不同的函数上锁,有的是通过CPU硬件的方式来实现,不必纠结这点。

2.3.1 atomic_flag

定义 std::atomic_flag odom_lock_ = ATOMIC_FLAG_INIT;
使用(两个成员函数)

  • odom_lock_.test_and_set()
    先获取状态作为返回值,然后设置成true(经过这个语句后flag一定是true)
  • odom_lock_.clear()
    清除 std::atomic_flag 对象的标志位,即设置 atomic_flag 的值为 false

3. 条件变量与信号量(控制线程通断)

3.1 条件变量

观察如下程序

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <deque>

std::mutex mtx;
std::deque<int> q;

//producer
void task1(){
	int i = 0;
	while (true){
		std::unique_lock<std::mutex> lock(mtx);
		q.push_back(i);
		if (i<9999){
			i++;
			}
		else{
			i = 0;
			}
		}
	}

//consumer
void task2(){
	int data = 0;
	while (true){
		std::unique_lock<std::mutex> lock(mtx);
		if (!q.empty()){
			data = q.front();
			q.pop_front();
			std::cout<<"get value from que :"<<data<<std::endl;
			}
		}
	}
int main(){
	std::thread t1(task1);
	std::thread t1(task2);
	t1.join();
	t2.join();
	}

该程序会因为task2循环不断地检查队列是否为空而消耗大量的CPU资源,因此需要寻找一种方式能够降低CPU资源占用——条件变量,通过条件变量可以使线程进入睡眠、唤醒线程。

  • 条件变量头文件:#include <condition_variable>
  • 定义方式:std::condition_variable name;
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <deque>
#include <condition_varibale>

std::mutex mtx;
std::deque<int> q;
std::condition_variable cv;

//producer
void task1(){
	int i = 0;
	while (true){
		std::unique_lock<std::mutex> lock(mtx);
		q.push_back(i);
		cv.notify_one();	//随机唤醒当前陷入休眠的一个线程
		//cv.notify_all();	//唤醒当前所有休眠的一个线程
		if (i<9999){
			i++;
			}
		else{
			i = 0;
			}
		}
	}

//consumer
void task2(){
	int data = 0;
	while (true){
		std::unique_lock<std::mutex> lock(mtx);
		if (q.empty()){
			cv.wait(lock);	//如果队列q为空,则该线程陷入休眠状态
							//此时不再需要锁lock,因此传入lock,cv.wait会将lock解锁
							//唤醒后是继续这条向下执行,还是从头执行函数task2有待验证
			}
		
		data = q.front();
		q.pop_front();
		std::cout<<"get value from que :"<<data<<std::endl;
		}
	}
int main(){
	std::thread t1(task1);
	std::thread t1(task2);
	t1.join();
	t2.join();
	}

3.2 信号量(唤醒特定数量的线程)

  • 信号量会在内部维护一个计数器,根据计数器的值来确定线程通断。
  • 头文件:#include <semaphore>
  • 定义方式
    std::counting _semaphore<MaxValue> csem(num1);
    std::binary_semaphore bsem(num1);
    其中后者是前者的特化,相当于std::counting _semaphore<1> bsem(num1);
    num1为内部计数器的初始值,MaxValue为内部计数器的最大值
  • 成员函数
    bsem、csem会在内部维护一个计数器
    当调用release(num2)时,内部计数器+num2,缺省为+1,注意num2需要小于MaxValue
    当调用acquire()时,内部计数器-1,当结果为负值时,线程便会阻塞在acquire处
	csem.acquire();
	csem.release(num2);
	bsem.acquire();
	bsem.release();

4.promise和future

4.1 简介

首先来看一段有问题的程序

例程4.1
#include <iostream>
#include <thread>
#include <future>

void task(int a,int b,int& ret){
		int ret_a = a*a;
		int ret_b = b*2;
		ret = ret_a+ret_b;
	}
int main(){
		int ret = 0;
		std::thread t(task,1,2,std::ref(ret));
		std::cout<<"the Value is"<<ret;
		t.join();
	}

在这段程序中,主线程和子线程都使用了公共资源ret,因此需要加锁。加锁不仅加大了代码量,还需要定义全局变量(std::mutex mtx)。
为解决这个问题,标准库提供了std::promise<>std::future<>两个模板类,具体用途及方法见例程。

例程4.2
#include <iostream>
#include <thread>
#include <future>

void task(int a,int b,std::promise<int>& ret){
		int ret_a = a*a;
		int ret_b = b*2;
		ret.set_value(ret_a+ret_b);//对p_ret进行赋值
	}
int main(){
		int ret = 0;
		std::promise<int> p_ret;
		std::future<int> f_ret = p_ret.get_future();//定义promise和future,并建立二者之间的联系
		
		std::thread t(task,1,2,std::ref(p_ret));//将p_ret以引用传递的方式传入
		/* do something else*/
		std::cout<<"the Value is"<<f_ret.get();//运行到future.get()的时候,程序会一直阻塞在这里,直到promise被set_value()
		t.join();
	}

注意,future的get()操作只能进行一次,否则程序会crash

考虑promise和future的其他用途,比如某个子线程需要两个参数,只有一个参数已知,另一个目前还未知,可以通过promise和future让子线程先对已知的部分进行计算,等到未知参数传入后再进行剩余部分的计算,具体例程如下

#include <iostream>
#include <thread>
#include <future>

void task(int a,std::future<int>& b,std::promise<int>& ret){
		int ret_a = a*a;
		int ret_b = b.get() * 2;	//子线程被阻塞在这里
		ret.set_value(ret_a+ret_b);//对p_ret进行赋值
	}
int main(){
		int ret = 0;
		std::promise<int> p_ret;
		std::future<int> f_ret = p_ret.get_future();//定义promise和future,并建立二者之间的联系

		std::promise<int> p_in;
		std::future<int> f_in;
		
		std::thread t(task,1,std::ref(f_in),std::ref(p_ret));//将p_ret、f_in以引用传递的方式传入
		/* do something else*/
		p_in.set_value(2);	//这里获得了第二个参数的值,set进去,此时被阻塞的子进程被激活
		std::cout<<"the Value is"<<f_ret.get();//运行到future.get()的时候,程序会一直阻塞在这里,直到promise被set_value()
		t.join();
	}

4.2 多个线程需要使用future

future对象的成员函数get()只能调用一次,那如果有很多线程需要那个数据呢——可以使用模板类std::shared_future<>

#include <iostream>
#include <thread>
#include <future>

void task(int a,std::shared_future<int> b,std::promise<int>& ret){
		int ret_a = a*a;
		int ret_b = b.get() * 2;	//子线程被阻塞在这里
		ret.set_value(ret_a+ret_b);//对p_ret进行赋值
	}
int main(){
		int ret = 0;
		std::promise<int> p_ret;
		std::future<int> f_ret = p_ret.get_future();//定义promise和future,并建立二者之间的联系

		std::promise<int> p_in;
		std::future<int> f_in;
		
		std::shared_future<int> s_f = f_in.share();
		
		std::thread t0(task,1,s_f,std::ref(p_ret));//将p_ret以引用方式传递,s_f以值传递方式传递
		std::thread t1(task,1,s_f,std::ref(p_ret));//s_f在值传递过程中,在每个线程中都被复制一份,因此不会影响get的使用
		std::thread t2(task,1,s_f,std::ref(p_ret));
		std::thread t3(task,1,s_f,std::ref(p_ret));
		/* do something else*/
		p_in.set_value(2);	//这里获得了第二个参数的值,set进去,此时被阻塞的子进程被激活
		std::cout<<"the Value is"<<f_ret.get();//运行到future.get()的时候,程序会一直阻塞在这里,直到promise被set_value()
		t.join();
	}

shared_future可以被复制,可以直接值传递。这时每个线程都会持有一份复制的shared_future,不必担心get的问题了。而promise和future都不可复制,必须使用引用传递,要想复制只能通过std::move来移动复制

4.3 复制性

promise和future都不可复制,必须使用引用传递,如果想要复制需要使用std::move
shared_future可以复制,可以使用值传递

5.std::async() 自由控制是否新建线程

利用第4节中提到的promise和future编写的程序仍不够简化,需要定义线程、promise、future,代码量仍然较大。可以利用标准库提供的函数std::async()进行简化,例程4.2可以简化为如下形式

#include <iostream>
#include <thread>
#include <future>

int task(int a,int b){
		int ret_a = a*a;
		int ret_b = b*2;

		return ret_a+ret_b;
	}
int main(){
/*
		int ret = 0;
		std::promise<int> p_ret;
		std::future<int> f_ret = p_ret.get_future();//定义promise和future,并建立二者之间的联系
		
		std::thread t(task,1,2,std::ref(p_ret));//将p_ret以引用传递的方式传入
		//do something else
		std::cout<<"the Value is"<<f_ret.get();//运行到future.get()的时候,程序会一直阻塞在这里,直到promise被set_value()
		t.join();
*/
		std::future<int> fu = std::async(task,1,2);
		std::cout<<"the value is"<<fu.get();
	}

std::async()的返回类型为std::future,参数列表为std::async(type,func,args),其中

  • type分为std::launch::asyncstd::launch::deferred,缺省为std::launch::deferred
    std::launch::async表示新开一个线程进行func函数的调用,而std::launch::deferred则不新开一个子线程,只有在std::async()返回的std::future进行get()调用的时候,才会进行计算。
    也就是说,使用std::async()并不等于新建了一个线程,需要自己指定是否新建线程,只有参数为std::launch::async的时候才在子线程进行计算
  • func是需要调用的函数名
  • args是func的参数列表
  • 注意此时的被调用函数task不再是void类型,而是int类型,使用async的时候不再需要引用传回函数计算结果,只需函数return需要的类型,再用同类型的std::future<>接受即可

6. std::packaged_task<> 任务封装

模板类std::packaged_task<>类似于std::bind(),但是更适合多线程使用,因为std::packaged_task<>能够与std::future联系起来,从而更方便多线程使用。

6.1 使用方式

std::packaged_task<RetType(argstype)> test(func_name);
func_name为需要调用的函数名
RetType为func_name的返回类型
argstypes为func的参数列表类型

例程

#include <iostream>
#include <thread>
#include <future>

int task(int a,int b){
		int ret_a = a*a;
		int ret_b = b*2;

		return ret_a+ret_b;
	}
int main(){
		std::packaged_task<int(int,int)> t(task);	//完成对任务的包装

		t(1,2);	//在其他地方完成任务调用
		std::cout<<"the value is"<<t.get_future().get()<<std::endl;	//利用成员函数get_future()获取future类对象,再get()到数值
	}

6.2 提前设定好某些参数——与std::bind()联合使用

#include <iostream>
#include <thread>
#include <future>

int task(int a,int b){
		int ret_a = a*a;
		int ret_b = b*2;

		return ret_a+ret_b;
	}
int main(){
		//std::packaged_task<int(int,int)> t(task);	//完成对任务的包装
		std::packaged_task<int> t(std::bind(task,1,2));	// 可以简化为auto t(std::bind(task,1,2))

		t();	//在其他地方完成任务调用
		std::cout<<"the value is"<<t.get_future().get()<<std::endl;	//利用成员函数get_future()获取future类对象,再get()到数值
	}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值