-
什么是C++多线程?
- 线程:线程是操作系统进行任务调度的基本单位。
- 进程:进程是操作系统进行资源分配的基本单位。
- 多线程:多线程是实现并行的手段,并行即多个线程同时执行
- C++多线程:(简单情况下)C++多线程使用多个函数实现各自功能,然后将不同函数生成不同线程,并同时执行这些线程(不同线程可能存在一定程度的执行先后顺序,但总体上可以看做同时执行)。
- 并行:同一时刻有复数的任务执行。
- 并法:同一时间段内有复数任务执行。
-
C++多线程基础
-
创建线程
-
- 头文件#include<thread>(C++11的标准库中提供了多线程库),该头文件中定义了thread类,创建一个线程即实例化一个该类的对象。
-
do_task(); std::thread(do_task);
- C++ 11的线程库创建一个std::thread对象,就会启动一个线程,并使用该std::thread对象来管理该线程。构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:
- lambda表达式
-
for (int i = 0; i < 4; i++) { thread t([i]{ cout << i << endl; }); t.detach(); }
- 重载了()运算符的类的实例
-
class Task { public: void operator()(int i) { cout << i << endl; } }; int main() { for (uint8_t i = 0; i < 4; i++) { Task task; thread t(task, i); t.detach(); } }
- 注意: C++的语法解析错误(C++'s most vexing parse)。向std::thread的构造函数中传入的是一个临时变量,而不是命名变量就会出现语法解析错误。如下代码:
-
std::thread t(Task()); // 等价于std::thread t(Task (*) (void));
这里相当于声明了一个函数t,其返回类型为thread,而不是启动了一个新的线程。可以使用新的初始化语法避免这种情况
-
std::thread t{Task()};
-
线程结束
- 当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。C++11有两种方式来等待线程结束:
- detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。
- join方式,等待启动的线程完成,才会继续往下执行。
- 注意:
- detach()使用不当会发生引用对象失效的错误。创建的新线程对当前作用域的变量的使用,创建新线程的作用域结束后,有可能线程仍然在执行,这时局部变量随着作用域的完成都已销毁,如果线程继续使用局部变量的引用或者指针,会出现意想不到的错误,并且这种错误很难排查。
- 当线程启动后,一定要在和线程相关联的thread对象销毁前,对线程运用join()或者detach()。如果一个 std::thread 对象在销毁时,关联的线程仍然是可连接状态,系统会调用 std::terminate() 终止程序。
- 可连接状态:
- 1.线程既没有使用 join() 使线程执行完毕
- 2.也没有使用detach()使线程完成分离
-
向线程传递参数
- 向线程调用的函数传递参数也是很简单的,只需要在构造thread的实例时,依次传入即可。需要注意的是,默认的会将传递的参数以拷贝的方式复制到线程空间,即使用传值的方式传参。
-
默认行为:参数拷贝
- 当你创建 std::thread 时,传递给线程函数的参数会默认被复制。这意味着即使你传递的是一个引用类型,std::thread 仍然会对其进行拷贝。这可能会导致你以为传递的是引用,实际上是传递了一个值拷贝。char*类型传入线程空间后,在线程的空间内转换为string。
-
传递引用:std::ref
- 如果你希望将参数以引用的方式传递给线程函数,可以使用 std::ref 或 std::cref(用于常量引用)。这告诉 std::thread 你想要传递。
-
void func(int *a,int n){} int buffer[10]; thread t(func,buffer,10);//传值 //thread t(func,std::ref(buffer),10); //传址 t.join();
-
转移线程的所有权
- thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach。
-
thread t1(f1); thread t3(move(t1));
将线程从t1转移给t3,这时候t1就不再拥有线程的所有权,调用t1.join或t1.detach会出现异常,要使用t3来管理线程。这也就意味着thread可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。
- 线程的标识类型为std::thread::id,有两种方式获得到线程的id。
- 通过thread的实例调用get_id()直接获取
- 在当前线程上调用this_thread::get_id()获取
-
互斥量
- 互斥量是用于保证多个线程,互斥地使用临界资源(共享资源)的信号量。互斥量保证了同一时刻只被一个进程使用。
- 程序实例化mutex对象m,线程调用成员函数m.lock()会发生下面 3 种情况
- 如果该互斥量当前未上锁,则调用线程将该互斥量锁住,直到调用unlock()之前,该线程一直拥有该锁。
- 如果该互斥量当前被锁住,则调用线程被阻塞,直至该互斥量被解锁。
- 不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock能避免忘记解锁这种问题。
-
#include<iostream> #include<thread> #include<mutex> using namespace std; mutex m;//实例化m对象,不要理解为定义变量 void proc1(int a) { m.lock(); cout << "proc1函数正在改写a" << endl; cout << "原始a为" << a << endl; cout << "现在a为" << 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(); proc2.join(); return 0; }
-
lock_guard
- 其原理(RAII一种编程范式)是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。加锁绑定于对象的创建,解锁绑定于对象的析构,利于栈自动回收临时变量的机制,通过使用{}来调整作用域范围(对象的生命周期),可使得互斥量m在合适的地方被解锁。
- lock_gurad也可以传入两个参数,第一个参数为adopt_lock标识时,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。
-
#include<iostream> #include<thread> #include<mutex> using namespace std; mutex m;//实例化m对象,不要理解为定义变量 void proc1(int a) { m.lock();//手动锁定 lock_guard<mutex> g1(m,adopt_lock); cout << "proc1函数正在改写a" << endl; cout << "原始a为" << a << endl; cout << "现在a为" << a + 2 << endl; }//自动解锁 void proc2(int a) { lock_guard<mutex> g2(m);//自动锁定 cout << "proc2函数正在改写a" << endl; cout << "原始a为" << a << endl; cout << "现在a为" << a + 1 << endl; }//自动解锁 int main() { int a = 0; thread proc1(proc1, a); thread proc2(proc2, a); proc1.join(); proc2.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;
|
|
|
|
|
|
|
|
|
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
cout << "不拉不拉不拉" << endl;
g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock();注意,不是m.lock()
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
g1.unlock();//临时解锁
cout << "不拉不拉不拉" << endl;
g1.lock();
cout << "不拉不拉不拉" << endl;
}//自动解锁
void proc2(int a)
{
unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
int a = 0;
thread proc1(proc1, a);
thread proc2(proc2, a);
proc1.join();
proc2.join();
return 0;
}
-
所有权的转移
- unique_lock<mutex> g2(m,defer_lock);
- unique_lock<mutex> g3(move(g2));
- 所有权转移,此时由g3来管理互斥量m
-
mutex m; { unique_lock<mutex> g2(m,defer_lock); unique_lock<mutex> g3(move(g2));//所有权转移,此时由g3来管理互斥量m g3.lock(); g3.unlock(); g3.lock(); }
-
condition_variable
- std::condition_variable 提供了一种机制,允许一个线程阻塞自己,直到某个条件被其他线程通知为止。典型的使用场景是一个或多个线程等待某个事件发生,比如等待一个队列中的数据变为可用,或者等待某个计算完成
- wait(locker): 如果locker处于未锁状态,wait() 函数的行为将是未定义的(可能导致程序崩溃)。因此,调用 wait() 之前必须确保 lock 已经锁定。
- 如果在线程被阻塞时,该函数会自动调用 locker.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(通常是另外某个线程调notify_* 唤醒了当前线程),wait() 函数此时再自动调用 locker.lock()。
- notify_once():随机唤醒一个等待的线程
- notify_all():唤醒所有等待的线程
-
async
- async是一个函数模板,用来启动一个异步任务,它返回一个future类模板对象,future对象起到了占位的作用,刚实例化的future是没有储存值的,但在调用future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给future,即通过FutureObject.get()获取函数返回值。
- 主线程中,async创建子线程并返回future对象,子线程运行时,主线程阻塞,直到子线程return,get()返回值时,主线程结束阻塞。
-
#include <iostream> #include <thread> #include <mutex> #include<future> #include<Windows.h> using namespace std; double t1(const double a, const double b) { double c = a + b; Sleep(3000);//假设t1函数是个复杂的计算过程,需要消耗3秒 return c; } int main() { double a = 2.3; double b = 6.7; future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位; cout << "正在进行计算" << endl; cout << "计算结果马上就准备好,请您耐心等待" << endl; cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return //cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。 return 0; }
-
shared_future
- future与shard_future的用途都是为了占位,但是两者有些许差别。
- future的get()成员函数是转移数据所有权;shared_future的get()成员函数是复制数据。
- 因此:future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。
- shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。
|
|
|
|
|
|
|
|
|
#include <iostream>
#include <thread>
#include <future>
// 一个简单的函数,模拟一些计算任务
int compute(int x) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
return x * x;
}
int main() {
// 创建一个异步任务,并获取其 future 对象
std::shared_future<int> sharedFuture = std::async(std::launch::async, compute, 10).share();
// 启动多个线程来读取 shared_future 的结果
std::thread t1([sharedFuture]() {
std::cout << "Thread 1 got result: " << sharedFuture.get() << std::endl;
});
std::thread t2([sharedFuture]() {
std::cout << "Thread 2 got result: " << sharedFuture.get() << std::endl;
});
std::thread t3([sharedFuture]() {
std::cout << "Thread 3 got result: " << sharedFuture.get() << std::endl;
});
// 等待所有线程完成
t1.join();
t2.join();
t3.join();
return 0;
}
-
原子automic
- 原子操作指“不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的。互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。automic是一个模板类,使用该模板类实例化的对象,提供了一些保证原子性的成员函数来实现共享数据的常用操作。
- automic对象提供了常见的原子操作(通过调用成员函数实现对数据的原子操作):
- store是原子写操作,load是原子读操作。exchange是于两个数值进行交换的原子操作。
- 即使使用了automic,也要注意执行的操作是否支持原子性。一般atomic原子操作,针对++,–,+=,-=,&=,|=,^=是支持的。