C++11线程库与异步函数----C++基础(第六期)

55 阅读9分钟

C++11 移动构造和完美转发

右值引用move()
int i=2;   //i叫左值,"2"叫右值
int &b=i;   //左值引用(给i所代表的内存块弄了个别名b)
int &&c=move(i)   //右值引用(给i所代表的内存块弄了个新名字c)
                 //原来的名字i不能再被使用。
//int &&c 需要一个右值即常量,但右值引用过后,
//正常使用c又会被当成左值

std::move(i); 作用:将i这个左值转变为右值(即将变量i所占有的内容的所属权拿出来)

个人理解:将i所代表的内存块的所属权搬出来,给予了另一个变量,此时i就被掏空了,就不能再代表该内存块了------该内存块有了一个新名字。

右值引用优点: 这样赋值更快,性能更好,没有拷贝与复制和开辟新的内存块。

完美转发forward()

目的:转发右值

通过forward()函数的处理,原来是左值的还是左值,原来是右值的还是右值,没有变化,这样进一步保证了速度与性能。

std::forward<变量类型>(变量); //返回值跟变量性质一致(左值还是右值)

template<typename T>
void PerfectForward(T &&i) {
	printValue(std::forward<T>(i)); //这个i会被当作右值处理
}

下面部分需要一定的操作系统基础,请谨慎食用!!!!

C++11 异步函数

头文件#include<future> std::future 用于保存异步函数返回的值(共享状态)(获得了值就是共享状态就绪)

获得future 方法1(函数只能执行一次)

future<返回值类型>res = async(函数名,参数1,参数2……); //res就是一个有效的future对象。

该语句作用:将函数变为一个异步任务执行(即一个新的线程)

res.wait();  //会阻塞线程,直到res获得函数返回值
res.get();   //取出异步函数返回值,res由有效future变为无效
res.valid();  //判断 res是否有效,有效返回ture,无效返回false
res.wait_for();  //传入一段时间,阻塞线程
                 //直到获得值或超过这段时间
res.wait_until();  //传入一个时间点,阻塞线程
                  //直到获得值或超过该时间点
                  
//async :创建有效future对象的方法1
       
       
future<int>res = async(launch::async,函数名,参数1,参数2……);   
//launch::async  启动策略

//launch::async  创建时便开始执行后面的函数

//launch::deferred  暂时不执行,直到.wait()或.get()开始
//获取返回值时(即开始执行.wait()或.get()语句),才开始执行函数。
获得future方法2(函数可以多次执行)

packaged_task: 包装函数,便于异步调用

packaged_task<函数返回值(参数类型1……)>Bb(函数名);
//将函数包装为Bb

future<函数返回值类型>res=Bb.get_future();
//创建有效future对象的方法2
//.get_future()  会返回一个与Bb相关联的future对象

Bb.valid(); //判断对象Bb是否有共享状态----即获得函数返回值
Bb.make_ready_at_thread_exit();  
//在线程退出时,才让共享状态就绪
Bb.reset(); //重置Bb,在关系不变的情况下,利用此可以多次调用函数

//每个packaged_task 只能调用一次.get_future();

例子:

int task(int i)
{
 cout<<666<<endl;
 return 0;
}
packaged_task<int(int)>pa(task);
future<int>fk=pa.get_future();

pa(1);    //该语句执行等同于task(1); 并且返回值已经存入fk中
fk.get(); //获取函数返回值

C++11 线程库

头文件#include<thread>

创建线程

普通函数创建线程:

thread 线程名(函数地址,参数1,参数2……); //有参

thread 线程名(函数地址); //无参

成员函数构建线程:

thread 线程名(&类名::函数名,&对象名,参数1,参数2……);
线程名.join();    //等待对应线程执行结束,阻塞函数,释放资源
线程名.joinable();    //判断对应线程执行结束,已结束返回ture
线程名.detach();    //线程分离,不需要join()等待

线程名.get_id();    //获得对应线程的线程号,
//若在线程对象对应的线程函数中获取本线程id,
//调用"this_thread::get_id()"

线程函数的参数为值传递,即使引用也是对线程中的参数值的引用,与外部实参无关,若想通过线程实参来影响外部实参:

  1. 直接传参数地址
  2. 借助ref()函数----std::ref(实参)
  3. 通过lambda函数,利用[&]对外部实参进行引用。
互斥量库(mutex)
互斥锁

std::mutex 互斥锁名.lock(); //加锁(阻塞函数)

std::mutex 互斥锁名.unlock(); //解锁

std::mutex 互斥锁名.try_lock(); //互斥量未锁则加锁,已锁则返回false

递归互斥锁
std::recursive_mutex 递归互斥锁名.lock();
//在递归函数中使用递归互斥锁,其允许**同一个线程**
//对互斥量多次上锁,但释放时,需要调用同样多次的
//std::recursive_mutex 递归互斥锁名.unlock();

std::recursive_mutex 递归互斥锁名.try_lock();  
//与互斥锁作用类似
定时锁

std::timed_mutex

std::timed_mutex 定时锁名.lock();  //加锁(阻塞函数)
std::timed_mutex 定时锁名.unlock();  //解锁
std::timed_mutex 定时锁名.try_lock();   
//互斥量未锁则加锁,已锁则返回false

std::timed_mutex 定时锁名.try_lock_for();  
//传入一个时间范围,该时间范围内未获得锁,就阻塞,
//若超过了该时间范围仍未获得锁,则返回false

std::timed_mutex 定时锁名.try_lock_untill(); 
//传入一个时间点,该时间点到来前未获得锁,就阻塞,
//若超过了该时间点仍未获得锁,则返回false
定时递归锁

std::recursive_timed_mutex

上面两者的结合。

智能锁(类似智能指针)
  • lock_guard
 创建: lock_guard<mutex>智能锁名(a);  //a是一个mutex变量

lock_guard对象定义时,就开始自动上锁,直到该对象析构时,自动解锁,若只想锁住部分代码,则需要用代码块{……}。

  • unique_lock 与lock_guard相同,但有更多的成员函数(用户可以自由控制加锁解锁)
创建: unique_lock<mutex>智能锁名(a);  //a是一个mutex变量
智能锁名.lock();  //加锁(阻塞函数)
智能锁名.unlock();  //解锁
智能锁名.try_lock();  
智能锁名.try_lock_for(); 
智能锁名.try_lock_untill(); 
智能锁名.release();   //释放对象所管理的互斥量指针,并释放所有权
智能锁名.owns_lock(); //判断当前对象是否上锁
智能锁名.mutex();    //返回对象所管理的互斥量指针
C++11 原子操作类型

线程对该类型变量访问时,自动进行互斥访问。(不需要互斥量)

原子操作类型定义需要大括号对其进行初始化。

普通类型: int 原子操作类型: atomic_int

普通类型: char 原子操作类型: atomic_char

普通类型: long 原子操作类型: atomic_long ……

也可以自定义原子操作类型: atomic<自定义类型>变量名

原子操作类型支持变量++,--,加一个值,减一个值,与,或,异或操作。

条件变量库(condition_variable)

头文件#include<condition_variable>

condition_variable中提供的成员函数,可分为wait系列和notify系列两类。

wait系列

wait系列成员函数的作用就是让调用线程进行阻塞等待,包括waitwait_forwait_until

//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class T>
void wait(unique_lock<mutex>& lck, T pred);
  • 调用第一个版本的wait函数时只需要传入一个互斥锁,线程调用wait后会立即被阻塞,直到被唤醒。
  • 调用第二个版本的wait函数时除了需要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象-----一般是一个匿名函数,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

为什么调用wait系列函数时需要传入一个互斥锁?

  • 因为wait系列函数一般是在临界区中调用的,为了让当前线程调用wait阻塞时其他线程能够获取到锁,因此调用wait系列函数时需要传入一个互斥锁,当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。
  • 因此wait系列函数实际上有两个功能,一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。

wait_for和wait_until函数的使用方式与wait函数类似:

  • wait_for函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个时间段,表示让线程在该时间段内进行阻塞等待,如果超过这个时间段则线程被自动唤醒。
  • wait_until函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个具体的时间点,表示让线程在该时间点之前进行阻塞等待,如果超过这个时间点则线程被自动唤醒。
  • 线程调用wait_for或wait_until函数在阻塞等待期间,其他线程调用notify系列函数也可以将该线程唤醒。此外,如果调用的是wait_for或wait_until函数的第二个版本的接口,那么当线程被唤醒后还需要调用传入的可调用对象-----一般是一个匿名函数,如果可调用对象的返回值为false,那么当前线程还需要继续被阻塞。

注意: 调用wait系列函数时,传入互斥锁的类型必须是unique_lock。

notify系列

notify系列成员函数的作用就是唤醒等待的线程,包括notify_onenotify_all

  • notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
  • notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

注意:  条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队。

本文C++11线程库参考文章