本文为教程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();
}
解决方法
- 不同函数中上锁的时候,保持同样的上锁顺序
- 使用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::async
和std::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()到数值
}