C++多线程学习笔记(三)

前面五一放假期间发表了两期文章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时才会被解除阻塞。

例子:

  1. 通知或者超时都会解锁,所以主线程会一直打印。
  2. 示例中只要过去一秒,就会不断的打印。

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。

一旦共享状态的标志变为 readywait() 函数返回,当前线程被解除阻塞,但是 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)

  1. promise 对象是异步 Provider,它可以在某一时刻设置共享状态的值。
  2. future 对象可以异步返回共享状态的值,或者在必要的情况下阻塞调用者并等待共享状态标志变为 ready,然后才能获取共享状态的值。

好啦,关于c++多线程的几种头文件的分享就到这啦,这些笔记可能不全,后面可能会陆续的补充。

本贴为博主亲手整理。如有错误,请评论区指出,一起进步。谢谢大家的浏览.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值