前面五一放假期间发表了两期文章c++多线程的学习笔记,之后又因为某些事停更了几天,今天我们继续来看看c++多线程问题。前面讲完了c++多线程的前两种头文件,今天我们来看看第三种头文件吧。
1.condition_variable
condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。condition_variable必须结合unique_lock使用。condition_variable_any可以使用任何的锁。
下面以condition_variable为例进行介绍。condition_variable条件变量可以阻塞(wait、wait_for、wait_until)调用的线程直到使用(notify_one或notify_all)通知恢复为止。condition_variable是一个类,这个类既有构造函数也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。
注意:notify_one()与notify_all()常用来唤醒阻塞的线程,线程被唤醒后立即尝试获得锁。
notify_one()因为只唤醒一个线程,不存在锁争用,所以能够立即获得锁。其余的线程不会被唤醒,等待再次调用notify_one()或者notify_all()。
notify_all()会唤醒所有阻塞的线程,存在锁争用,只有一个线程能够获得锁。那其余未获取锁的线程接着会怎么样?会阻塞?还是继续尝试获得锁?答案是会阻塞,等待操作系统在互斥锁的状态发生改变时唤醒线程。当持有锁的线程释放锁时,操作系统会唤醒这些阻塞的线程,而这些线程会继续尝试获得锁。
(1)wait
当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。
在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。
例子:
(2)wait_for
与std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。 而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。
下面是wait_for的模板函数参数
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time);
下面是重载版本:
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred);
另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。
例子:
- 通知或者超时都会解锁,所以主线程会一直打印。
- 示例中只要过去一秒,就会不断的打印。
2.atomic:
原子类型在头文件<atomic>
中,使用atomic
有两套命名模式:一种是使用替代名称,一种是使用atomic
的特化。
通常atomic
仅限一下这几种操作:load()
,store()
,exchange()
,compare_exchange_weak()
,compare_exchange_strong()
。
成员函数:
(1)load():加载原子对象中存入的值,等价与直接使用原子变量。
atomic<bool> b;
bool x = b.load(memory_order_acquire);
(2)store():存储一个值到原子对象,等价于使用等号。而且还可以添加内存模型参数,(这里就不说了)
b.store(true);
例子:
void fun_2()
{ atomic<int> b;
b.store(10);
cout << b.load() << endl;
b = 50;
cout << b << endl; }
(3)exchange
返回原来里面存储的值,然后在存储一个新的值,相当于将上面两个load()
和store()
合成起来的参数。
void fun_2()
{ atomic<int> b;
b.store(10);
cout << b.exchange(100) << endl;
cout << b; }
但是exchange()
是作为一个原子操作,而下面两个单独的组合却是两个的原子操作的组合,不再是原子操作。
(4)compare_exchange_weak()
交换-比较操作是比较原子变量值和所提供的期望值,如果二者相等,则存储提供的期望值,如果不等则将期望值更新为原子变量的实际值,更新成功则返回true(在bool类型下值为1)
反之则返回false(在bool类型下值为0)
。
当存储的值和expected
相等时则将则更新为new_value
,如果不等时则不变。其中expected
必须是类型变量,而不能是常量。compare_exchange_weak
可能出现即使原始值和期望值相等时,也有可能出现存储不成功,这种情况下变量的值将不会改变,并且返回false
。 这就是 伪失败
(5)compare_exchange_strong()
不像compare_exchange_weak,此版本必须始终true在预期确实与所包含的对象相等时返回,不允许出现虚假故障。但是,在某些计算机上,对于某些在循环中进行检查的算法,compare_exchange_weak 可能会明显改善性能。
其余使用方法和compare_exchange_weak完全一致。
原子操作,一般都是指“不可分割的操作”,也就是说这种操作状态要么是完成的,要么是没完成的,不可能出现半完成状态;
std::atomic代表原子操作,是一个类模板,其实这个东西用来封装某个类型的值的。
注意和互斥量的区分,互斥量的加锁一般都是针对一个代码段(几行代码),而原子操作针对的一般都是一个变量,而不是一个代码段,是无锁的。
划重点:使用原子操作跟使用mutex对比,可以提高效率。下面看一个简单的例子,就是共同写一个数字,然后最后访问。
(1)使用原子操作:
#include <mutex>
#include <thread>
#include <iostream>
#include<atomic>
using namespace std;
std::atomic<int> ncount = 0;
void testx()
{
clock_t t1 = clock();
for (int i = 0; i < 10000000; i++)
{
ncount++;
}
clock_t t2 = clock();
double totaltime = (t2 - t1) * 1.0 / CLOCKS_PER_SEC;
cout << "执行时间:" << totaltime << "秒\n";
}
void main()
{
std::thread t(testx);
std::thread t1(testx);
t.join();
t1.join();
cout << "ncount:" << ncount << endl;
system("pause");
}
(2)使用mutex:
#include <mutex>
#include <thread>
#include <iostream>
using namespace std;
int ncount = 0;
std::mutex mutex1;
void testx()
{
clock_t t1 = clock();
for (int i = 0; i < 10000000; i++)
{
mutex1.lock();
ncount++;
mutex1.unlock();
}
clock_t t2 = clock();
double totaltime = (t2 - t1) * 1.0 / CLOCKS_PER_SEC;
cout << "执行时间:" << totaltime << "秒\n";
}
void main()
{
std::thread t(testx);
std::thread t1(testx);
t.join();
t1.join();
cout << "ncount:" << ncount << endl;
system("pause");
}
做对比可以很明显的知道,这两种方法执行效率是不同的。原子操作的效率更高。
atomic的成员函数
3.future
引入一个全新的接口,让被调用的线程自动进行,调用线程直接调用结果就行。这就是C++引入的future()类
,而且这种方式可以在不同线程之间传递数据.future是一个模板类,用来获取异步任务的结果,其可以异步access共享状态。
成员函数:
(1)std::future::get()
(1) 当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。
(2) 如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。
(3) 当共享状态就绪后,则该函数将取消阻塞并返回(或抛出),同时释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次。
(4) std::future<void>::get()
不返回任何值,但仍等待共享状态就绪并释放它。
(2)std::future::valid()
作用:检查当前的std::future对象是否有效,有效的future是与共享状态(shared state)关联的future对象。
(3)std::future::share()
作用:获取共享的future,返回一个std::shared_future对象,该对象获取future对象的共享状态。future对象将不再有效。
(4)std::future::wait()
作用:
(1) 等待共享状态就绪,未就绪则该函数将阻塞调用的线程直到就绪。
(2) 当共享状态就绪后,则该函数将取消阻塞并void返回。
等待与当前 std::future
对象相关联的共享状态的标志变为 ready
.
如果共享状态的标志不是 ready
(此时 Provider 没有在共享状态上设置值(或者异常)),调用该函数会被阻塞当前线程,直到共享状态的标志变为 ready。
一旦共享状态的标志变为 ready
,wait()
函数返回,当前线程被解除阻塞,但是 wait()
并不读取共享状态的值或者异常。
(5)std::future::wait_for()
作用:
(1) 等待共享状态ready或超时。
(2) 此函数的返回值类型为枚举类future_status。
与 std::future::wait()
的功能类似,即等待与该 std::future
对象相关联的共享状态的标志变为 ready
,
而与 std::future::wait()
不同的是,wait_for()
可以设置一个时间段 rel_time
,如果共享状态的标志在该时间段结束之前没有被 Provider 设置为 ready
,则调用 wait_for
的线程被阻塞,在等待了 rel_time
的时间长度后 wait_for()
返回,
最后还有一个重要的类对象promise。这个类对象头文件为<future>
Promise 对象可以保存某一类型 T 的值,该值可被 future 对象读取(可能在另外一个线程中),因此 promise 也提供了一种线程同步的手段。在 promise 对象构造时可以和一个共享状态(通常是std::future)相关联,并可以在相关联的共享状态(std::future
)上保存一个类型为 T 的值。
可以通过 get_future
来获取与该 promise 对象相关联的 future 对象,调用该函数之后,两个对象共享相同的共享状态(shared state)
- promise 对象是异步 Provider,它可以在某一时刻设置共享状态的值。
- future 对象可以异步返回共享状态的值,或者在必要的情况下阻塞调用者并等待共享状态标志变为 ready,然后才能获取共享状态的值。
好啦,关于c++多线程的几种头文件的分享就到这啦,这些笔记可能不全,后面可能会陆续的补充。
本贴为博主亲手整理。如有错误,请评论区指出,一起进步。谢谢大家的浏览.