前言:
C++ 11通过标准库引入了对多线程的支持,这个是c++的新特性之一,也就是说我们直接用即可,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念(这个后文会讲到)。线程啥的就不再解释了,直接上干货;
头文件一定记得写如下几个:
#include <thread> //线程库
#include <condition_variable> //条件变量
#include <mutex> //互斥锁
1. 线程库函数的使用:
函数 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否是有效的,joinable代表的是一个正在执行中的线程。 |
join() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与主线程分离,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关。 |
注意:
- 线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
- 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
- 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,其实际引用的是线程栈中的拷贝,而不是外部实参。(也就是说创建thread对象进行绑定时,哪怕你的形参事引用都不会改变当前函数的变量值,有例子)
- 一个线程对象只能使用一次join(),不然程序会崩溃;在线程对象销毁前,要么以jion()的方式等待线程结束,要么以detach()的方式。
样例1(对应上面第三点):
void Fun1(int& x)
{
x += 20;
}
void Fun2(int* x)
{
*x += 20;
}
int main()
{
int a = 10;
thread t1(Fun1, a);
t1.join();
//线程函数参数尽管是引用方式,实际引用的是线程栈中的拷贝
cout << a << endl; // 10
// 如果想要通过形参改变外部实参时,怎么办呢?这时借助std::ref()函数
thread t3(Fun1, std::ref(a));
t3.join();
cout << a << endl; //30
thread t2(Fun2, &a);
t2.join();
cout << a << endl; //50
return 0;
}
2. 原子操作
C++11标准定义“原子类型”,可以保证原子类型在线程间被互斥的访问。
atomic_bool abool; //对应bool
atomic_char achar; //char
atomic_schar aschar; //signed char
atomic_uchar auchar; //unsigned char
atomic_int aint; //int
atomic_uint auint; //unsigned int
atomic_short ashort; //short
atomic_ushort aushort; //unsigned short
atomic_long along; //long
atomic_ulong aulong; //unsigned long
atomic_llong allong; //long long
atomic_ullong aullong; //unsigned long long
atomic_char16_t achar16_t; //char16_t
atomic_char32_t achar32_t; //char32_t;
atomic_wchar_t awchar_t; //wchar_t
但是,我们应该使用atomic类模板。通过该模板,可以定义出任意需要的原子类型:
std::atomic< type > t;
对线程而言,原子类型通常属于“资源型”的数据,这意味着多个线程通常只能访问的原子类型的拷贝。所以在C++11中,原子类型只能从其模板参数类型中进行构造,标准不允许原子类型经行拷贝构造、移动构造,以及operator=等,防止以外发生;
举个例子:
atomic< float > af{ 1.2f };
//atomic< float > af1{ af }; //这里无法编译
原因:atomic模板类的拷贝构造函数、移动构造函数、operator=等总是默认被删除的
在C++11中,标准将原子操作定义为atomic模板类的成员函数,这囊括了绝大多数典型的操作,如读、写、交换等,当然,对于内置类型而言,主要是通过重载一些全局操作符来完成的,在编译的时候,会产生一条特殊的lock前缀的x86指令,lock能够控制总线及实现x86平台上的原子性。
上面的那些是原子类型的函数的操作:读(load)、写(store)、交换(exchange)、比较并交换(compare_exchange_weak/compare_exchange_stronge)等操作;
当然,有时编译器会给我们作出优化:
atomic<int> a;
a = 1; //a.store(1);
int b = a; //b = a.load();
上图中,那个atomic_flag,这个要特别关注一下,听说效率很高,可以自制自旋锁,如下:
void Lock(atomic_flag *lock) {
while (lock->test_and_set()); }
void Ublock(atomic_flag *lock) {
lock->clear(); }
//test_and_set()函数是设置true值,返回之前的值。
//clear()是复位,置为false;
std::atomic_flag lock = ATOMIC_FLAG_INIT; //初始化
代码演示:
// 自旋锁实现.cpp : 定义控制台应用程序的入口点。
//
#include <iostream>
#include <atomic>
#include <thread>
#include <Windows.h>
using namespace std;
std::atomic_flag lock = ATOMIC_FLAG_INIT; //声明了全局变量,初始化为值ATOMIC_FLAG_INIT,即false状态
void Lock(atomic_flag *lock) {
while (lock->test_and_set()){
cout<<"Waiting..."<<endl;
} }
void Ublock(atomic_flag *lock) {
lock->clear(); }
void func(){
Lock(&lock);
cout << "func working..." << endl;
Ublock(&lock);
}
void foo(){
Lock(&lock);
cout<<"foo working..."<<endl;
Ublock(&lock);
}
int main(void)
{
std::thread t1(func);
std::thread t2(foo);
t1.join();
t2.join();
system("pause");
return 0;
}
截图:
原子类型有一些枚举值,这个可以稍微了解一下。
高级用法:
一、多线程启动函数:std::async()
声明方式:
template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type>
async (launch policy, Fn&& fn, Args&&... args);
其中:
// 异步启动的策略
enum class launch {
// 异步启动,在调用std::async()时创建一个新的线程以异步调用函数,并返回future对象;
async = 0x1,
// 延迟启动,在调用std::async()时不创建线程,直到调用了future对象的get()或wait()方法时,才创建线程;
deferred = 0x2,
// 自动,函数在某一时刻自动选择策略,这取决于系统和库的实现,通常是优化系统中当前并发的可用性
any = async | deferred,
sync = deferred
};
//参数 fn 是要调用的可调用 (Callable) 对象
//参数args 是传递给 f 的参数
//std::launch::async:在调用async就开始创建线程。
//std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
异步调用,当然可以大大提高程序运行的效率~
std::async()是一个接受回调(函数或函数对象)作为参数的函数模板,通过启动一个新线程或者复用一个它认为合适的已有线程异步调用。
std::async返回一个std::future< T >,它存储由std::async()执行的函数对象返回的值。所以通常都有std::future伴随着使用,因为future中存储了线程函数返回的结果。
内部原理:
std::async先异步操作用std::packaged_task包装线程函数,然后将异步操作的结果放到std::promise中,最后再通过future.get/wait来获取这个未来的结果。(后面会讲到)
二、std::future
std::future提供了一种访问异步操作结果的机制,也就是我们可以通过这个类对象,异步访问被调用线程函数的结果;
是类模板:
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
获取future结果有三种方式:
- get:等待异步操作结束并返回结果(会阻塞当前调用函数)
- wait:等待异步操作完成,没有返回值 (同上)
- wait_for是超时等待返回结果。
也可以通过查询future的状态(future_status)来获取异步操作的结果。future_status有三种状态:
- deferred:异步操作还没开始
- ready:异步操作已经完成
- timeout:异步操作超时
例子:
//查询future的状态
std::future_status status;
do {
status = future.wait_for(std::chrono::seconds(1)); //等待一秒
if (status == std::future_status::deferred) {
std::cout << "deferred\n";
} else if (status == std::future_status::timeout) {
std::cout << "timeout\n";
} else if (status == std::future_status::ready) {
std::cout << "ready!\n";
}
} while (status != std::future_status::ready);
代码演示:
# include <iostream>
# include <ctime>
# include <future>
# include <thread>
using namespace std;
int funca(int a,int b){
return a+b;
}
int funcb(int a){
return a;
}
int main(void)
{
future<int> f1 = std::async(funca,1,2); //<type> 是绑定的函数返回值
future<int> f2 = std::async(funcb,3);
auto it = f1.get() + f2.get();
cout<<it<<endl;
system("pause");
return 0;
}
三、std::promise
std::promise可以获取线程函数里的值,不过要等执行完毕后才可以获取;当然,是间接地通过promise内部提供的future来获取的!
用法:
std::promise<int> pr;
std::thread t([](std::promise<int>& p){ p.set_value_at_thread_exit(9); },std::ref(pr));
std::future<int> f = pr.get_future();
四、std::packaged_task
这个是包装了一个可调用对象(如function, lambda expression, bind expression, or another function object);packaged_task保存的是一个函数。
用法:
std::packaged_task<int()> task([](){ return 7; });
std::thread t1(std::ref(task));
std::future<int> f1 = task.get_future();
auto r1 = f1.get();
注:一般来说,用std::future以及std::async这两个用法即可。
锁
锁: 最常见的就是mutex (有RAII思想的管理锁的类模板,可以预防我们忘记解锁)
C++11根据mutext的属性提供四种的互斥量,分别是:
- std::mutex,最常用,普遍的互斥量(默认属性),
- std::recursive_mutex ,递归锁,允许同一线程使用recursive_mutext多次加锁,然后使用相同次数的解锁操作解锁。mutex多次加锁会造成死锁
- std::timed_mutex,在mutex上增加了时间的属性。增加了两个成员函数try_lock_for(),try_lock_until(),分别接收一个时间范围,再给定的时间内如果互斥量被锁主了,线程阻塞,超过时间,返回false。
- std::recursive_timed_mutex,增加递归和时间属性
时间锁:
timed_mutex myMutex;
chrono::milliseconds timeout(1000); //1秒
if (myMutex.try_lock_for(timeout))
{
//在1秒内获取了锁
//业务代码
myMutex.unlock();
}
else
{
//在100毫秒内没有获取锁
//业务代码
}
mutex成员函数(常用):
6. lock(),互斥量加锁,如果互斥量已被加锁,线程阻塞
7. bool try_lock(),尝试加锁,如果互斥量未被加锁,则执行加锁操作,返回true;如果互斥量已被加锁,返回false,线程不阻塞。
8. void unlock(),解锁互斥量
mutex RAII式的加锁解锁
std::lock_guard:
管理mutex的类。以独占所有权的方式管理mutex对象的上锁和解锁操作,对象构建时传入mutex,会自动对mutex加入,直到离开类的作用域,析构时完成解锁。RAII式的栈对象能保证在异常情形下mutex可以在lock_guard对象析构被解锁。
源码:
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx还没有被上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{
}
~lock_guard()
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
例子:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mut;
void Print(int num)
{
std::cout << "this is thread_unlock: " <<num<< std::endl;
{
std::lock_guard<std::mutex> lg(mut);//初始化就上锁
std::cout << "this is thread: " << num << std::endl;
}//离开块作用域就自动解锁
}
int main()
{
std::thread t1(Print, 1);
std::thread t2(Print, 2);
t1.join();
t2.join();
std::cout << "this is main thread " << std::endl;
return 0;
}
std::unique_lock:
也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。这个比较灵活,可以让我们指定“何时”以及“如何”锁定和结果Mutex,有挺多函数给我们进行选择。
总述:
条件变量(必须先加锁)
头文件:# include < condition_variable >
std::condition_variable readyCondVar;
条件的检测是在互斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量。如果条件为假,这个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量 可以被用来实现这两进程间的线程同步。
常用API接口:
代码:
std::mutex mutex;
std::condition_variable cv;
// 条件变量与临界区有关,用来获取和释放一个锁,因此通常会和mutex联用。
std::unique_lock lock(mutex); //和RALL锁机制使用
// 此处会释放lock,然后在cv上等待,直到其它线程通过cv.notify_xxx来唤醒当前线程,
// cv被唤醒后会再次对lock进行上锁,然后wait函数才会返回。
// wait返回后可以安全的使用mutex保护的临界区内的数据。此时mutex仍为上锁状态
cv.wait(lock)
(上面的wait这个函数可能会导致惊群效应,所以我们可以用重载版本,cv.wait(lock,可调用函数对象));
类似这样:
> g_cv.wait(lock, [] ){
return xxx; });
参考此篇文章:请点击!
notify_one()与notify_all()
点击链接查看这个知识点。(这篇博客也不严谨,我是持怀疑态度。。。)
参考文章:
转载:https://blog.csdn.net/weixin_43743711/article/details/115709025