当前位置:主页 > 查看内容

C++11的线程库

发布时间:2021-08-05 00:00| 位朋友查看

简介:前言 C 11通过标准库引入了对多线程的支持这个是c的新特性之一也就是说我们直接用即可使得C在并行编程时不需要依赖第三方库而且在原子操作中还引入了原子类的概念这个后文会讲到。线程啥的就不再解释了直接上干货 头文件一定记得写如下几个 #include thread……

前言:

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()在创建线程对象后马上调用,用于把被创建线程与主线程分离,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关。

注意:

  1. 线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
  3. 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,其实际引用的是线程栈中的拷贝,而不是外部实参。(也就是说创建thread对象进行绑定时,哪怕你的形参事引用都不会改变当前函数的变量值,有例子)
  4. 一个线程对象只能使用一次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结果有三种方式:

  1. get:等待异步操作结束并返回结果(会阻塞当前调用函数)
  2. wait:等待异步操作完成,没有返回值 (同上)
  3. wait_for是超时等待返回结果。

也可以通过查询future的状态(future_status)来获取异步操作的结果。future_status有三种状态:

  1. deferred:异步操作还没开始
  2. ready:异步操作已经完成
  3. 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);

std::chrono知识点

代码演示:

# 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的属性提供四种的互斥量,分别是:

  1. std::mutex,最常用,普遍的互斥量(默认属性),
  2. std::recursive_mutex ,递归锁,允许同一线程使用recursive_mutext多次加锁,然后使用相同次数的解锁操作解锁。mutex多次加锁会造成死锁
  3. std::timed_mutex,在mutex上增加了时间的属性。增加了两个成员函数try_lock_for(),try_lock_until(),分别接收一个时间范围,再给定的时间内如果互斥量被锁主了,线程阻塞,超过时间,返回false。
  4. std::recursive_timed_mutex,增加递归和时间属性

在这里插入图片描述

时间锁:

timed_mutex myMutex;
chrono::milliseconds timeout(1000);  //1秒
if (myMutex.try_lock_for(timeout))
{
	//在1秒内获取了锁
	//业务代码
	myMutex.unlock(); 
}
else
{
	//在100毫秒内没有获取锁
	//业务代码
}

time_mutex博客

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()

点击链接查看这个知识点。(这篇博客也不严谨,我是持怀疑态度。。。)



参考文章:

  1. 博客一
;原文链接:https://blog.csdn.net/weixin_43743711/article/details/115709025
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐