小言_互联网的博客

C++11的线程库

521人阅读  评论(0)

前言:

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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场