参考:
C++多线程详细讲解
C++ thread用法总结(整理)
C++ | 多线程编程(一)thread库的使用示例
1. 什么是C++多线程
线程: 线程是操作吸引能够进行运算调度的最小单位,它被包含早进程之中,进程包含一个或者多个线程。进程可以理解为完成一件事的完整解决方案,而线程可以理解为这个解决方案中的一个步骤,可能这个解决方案就这只有一个步骤,也可能这个解决方案有多个步骤。
多线程: 多线程是实现并发(并行)的手段,并发(并行)即多个线程同时执行,一般而言,多线程就是把执行一件事情的完整步骤拆分为多个子步骤,然后使得这个步骤同时执行。
C++多线程:(简单情况下)C++多线程使用多个函数实现各自功能,然后将不同函数生成不同线程,并同时执行这些线程(不同线程可能存在一定程度的执行先后顺序,但总体上可以看做同时执行)。
2. C++多线程基础知识
2.1 创建线程
创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名。
thread th1(proc1);
如果传递进去的函数本身需要传递参数,实例化对象时将这些参数按序写到函数名后面。
thread th1(proc1,a,b);
只要创建了线程对象(传递“函数名/可调用对象”作为参数的情况下),线程就开始执行(std::thread 有一个无参构造函数重载的版本,不会创建底层的线程)。
2.2 线程阻塞方法
join()与detach()
阻塞线程的目的是调节各线程的先后执行顺序,这里重点讲join()方法,不推荐使用detach(),detach()使用不当会发生引用对象失效的错误。
当线程启动后,一定要在和线程相关联的thread对象销毁前,对线程运用join()或者detach()。
join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续。th1.join(),即该语句所在的线程(该语句写在main()函数里面,即主线程内部)暂停,等待指定线程(指定线程为th1)执行结束后,主线程再继续执行。
整个过程就相当于你在做某件事情,中途你让老王帮你办一个任务(你办的时候他同时办)(创建线程1),又叫老李帮你办一件任务(创建线程2),现在你的这部分工作做完了,需要用到他们的结果,只需要等待老王和老李处理完(join(),阻塞主线程),等他们把任务做完(子线程运行结束),你又可以开始你手头的工作了(主线程不再阻塞)。
#include<thread>
#include<iostream>
using namespace std;
void proc(int a) {
cout << "我是子线程,传入参数为:" << a << endl;
cout << "子线程中显示子线程id为:" << this_thread::get_id() << endl;
}
int main() {
cout << "我是主线程" << endl;
int a = 9;
thread th2(proc, a);
th2.join();//主线程被阻塞 直到调用的子线程结束
cout << "主线程中显示的子线程id为" << th2.get_id() << endl;
return 0;
}
输出结果:
2.3 互斥量的使用
这样比喻,单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock),那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。
程序实例化mutex对象m,线程调用成员函数++m.lock()++会发生下面 3 种情况:
(1)如果该互斥量当前未上锁,则调用线程将该互斥量锁住,直到调用unlock()之前,该线程一直拥有该锁。
(2)如果该互斥量当前被锁住,则调用线程被阻塞,直至该互斥量被解锁。
互斥量怎么使用?
首先需要#include
lock()与unlock():
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a) {
m.lock();
cout << "proc1函数正在改写a" << endl;
cout << "原始a为:" << a << endl;
cout << "现在改写为:" << a + 2 << endl;
m.unlock();
}
void proc2(int a) {
m.lock();
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
m.unlock();
}
int main() {
int a = 0;
thread proc1(proc1, a);
thread proc2(proc2, a);
proc1.join();//proc线程阻塞主线程
proc2.join();
return 0;
}
不推荐实直接去调用成员函数lock(),lock()在被调用的函数里面写,因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock能避免忘记解锁这种问题。
lock_guard():
其原理是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁。从而使用lock_guard()就可以替代lock()与unlock()。
通过设定作用域,使得lock_guard在合适的地方被析构(在互斥量锁定到互斥量解锁之间的代码叫做临界区(需要互斥访问共享资源的那段代码称为临界区),临界区范围应该尽可能的小,即lock互斥量后应该尽早unlock),通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void prod1(int a) {
lock_guard<mutex> g1(m);
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
}
void prod2(int a) {
{
lock_guard<mutex> g2(m);
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}
cout << "作用域外的内容3" << endl;
cout << "作用域外的内容4" << endl;
cout << "作用域外的内容5" << endl;
}
int main() {
int a = 0;
thread proc1(prod1, a);
thread proc2(prod2, a);
proc1.join();
proc2.join();
return 0;
}
输出:
lock_gurad也可以传入两个参数,第一个参数为adopt_lock标识时,此时需要提前手动锁定。
lock_guard<mutex> g1(m,adopt_lock);
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void prod1(int a) {
//手动上锁
m.lock();
lock_guard<mutex> g1(m, adopt_lock);
cout << "proc1函数正在改写a" << endl;
cout << "1原始a为" << a << endl;
cout << "1现在a为" << a + 2 << endl;
}//自动解锁
void prod2(int a) {
lock_guard<mutex> g2(m);//自动上锁
cout << "proc2函数正在改写a" << endl;
cout << "2原始a为" << a << endl;
cout << "2现在a为" << a + 1 << endl;
}//自动解锁
int main() {
int a = 0;
thread proc1(prod1, a);
thread proc2(prod2, a);
proc2.join();//虽然是proc2先阻塞主线程,但是不影响最后的结果
proc1.join();
return 0;
}
unique_lock:
unique_lock类似于lock_guard,只是unique_lock用法更加丰富,同时支持lock_guard()的原有功能。
使用lock_guard后不能手动lock()与手动unlock();
使用unique_lock后可以手动lock()与手动unlock();
unique_lock的第二个参数,除了可以是adopt_lock,还可以是try_to_lock与defer_lock;
try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
defer_lock: 始化了一个没有加锁的mutex;
是否支持手动加锁 | 参数 | |
---|---|---|
lock_guard | 不支持(传入adopt_lock可以手动加锁) | adopt_lock |
unique_lock | 支持 | adopt_lock/try_to_lock/defer_lock |
try_to_lock是尝试加锁,所以打印的结果是随机的。
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;
mutex m;
void prod1(int a) {
//初始化一个没有加锁的mutex
unique_lock<mutex> g1(m, defer_lock);
cout << "*********" << endl;
//注意不是m.lock 是 g1.lock
g1.lock();
cout << "prod1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
g1.unlock();
cout << "************" << endl;
g1.lock();
cout << "*************" << endl;
}//自动解锁
void prod2(int a) {
//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
//unique_lock<mutex> g2(m, try_to_lock);
//由于是尝试加锁 所以如果不成功的话,打印出来的顺序是随机的。
unique_lock<mutex> g2(m);
//还可以使用move来转移锁的所有权
//unique_lock<mutex> g3(move(g2));
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main() {
int a = 0;
thread proc1(prod1, a);
thread proc2(prod2, a);
proc1.join();
proc2.join();
return 0;
}
2.4 condition_variable
需要#include<condition_variable>;
wait(locker):在线程被阻塞时,该函数会自动调用 locker.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(通常是另外某个线程调用 notify_* 唤醒了当前线程),wait() 函数此时再自动调用 locker.lock()。
- notify_once():随机唤醒一个等待的线程
- notify_all():唤醒所有等待的线程
2.5 异步线程
async与future:
async是一个函数模板,用来启动一个异步任务,它返回一个future类模板对象,future对象起到了占位的作用,刚实例化的future是没有储存值的,但在调用future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给future,即通过FutureObject.get()获取函数返回值。
相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(async创建子线程),前台给了你一个单据(future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去前台取结果,但是结果还没出来(子线程还没return),你就在前台等着(阻塞),直到你拿到结果(get())你才离开(不再阻塞)。
#include<iostream>
#include<thread>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<future>
using namespace std;
double t1(const double a, const double b) {
double c = a + b;
Sleep(3000);
return c;
}
int main() {
double a = 2.3;
double b = 6.7;
future<double> fu = async(t1, a, b);
cout << "正在进行计算" << endl;
cout << "计算结果马上就准备好,请您耐心等待" << endl;
cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
return 0;
}
输出结果:
shared_future
future与shard_future的用途都是为了占位,但是两者有些许差别。
future的get()成员函数是转移数据所有权;shared_future的get()成员函数是复制数据。
因此:
future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。
shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。
语义 | 是否可以多次调用 | |
---|---|---|
future | 转移所有权 | 赋值 |
shared_future | 否 | 是 |
#include<iostream>
#include<thread>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<future>
using namespace std;
mutex m;
double fun(double a, double b) {
cout << "fun函数调用" << endl;
cout << "计算返回值倒数3 2 1.." << endl;
Sleep(3000);
return a + b;
}
void t1(double a, double b) {
lock_guard<mutex> g1(m);//自动加锁
cout << "t1获取函数的返回值" << endl;
//得到fun函数返回值的一份拷贝
shared_future<double> sf1 = async(fun, a, b);
cout << "t1调用结果为:" << sf1.get() << endl;
}//自动解锁
void t2(double a, double b) {
lock_guard<mutex> g1(m);
cout << "t2获取函数的返回值" << endl;
shared_future<double> sf1 = async(fun, a, b);
cout << "t2调用结果为:" << sf1.get() << endl;
}
int main() {
double a = 2.3;
double b = 6.7;
thread porc1(t1, a, b);
thread proc2(t2, a, b);
porc1.join();
proc2.join();
return 0;
}
输出结果:
评论区