美文网首页C++多线程
C++11: 多线程简明指南

C++11: 多线程简明指南

作者: 小城大麦 | 来源:发表于2015-12-23 11:07 被阅读3832次

    std::async

    异步调用一个callable的对象,但是不保证调用时机。可以通过传入std::launch::async/deferred来制定是否立即调用和延迟(由系统决定调用时机)。
    async不保证内部使用线程池来实现,但是肯定使用一个后台线程执行任务。

    std::future

    类似于Java的future,获得任务执行结果,对于无返回值得结果使用future<void>,如果调用future的get方法,可以保证async的会立即执行。async()方法返回值是future.

    int sum(int a,int b){return a+b;}
    //如果async的返回值没有被使用,那么async将一直阻塞
    //编译器可以优化为 std::async(sum,1,1).get(); 阻塞调用
    std::future<int> f = std::async(sum,1,1);
    int result = f.get();//获得结果
    //f.wait() 等运行完成
    //f.wait_for(dur) 等待一定时间或运行完成
    //f.wait_until(fp) 等待某一时刻或运行完成
    //f.share() 生成一个shared_future
    

    std::packaged_task

    类似于async,但是可以自己控制调用时机,也可以使用线程去调用任务执行。
    std::packaged_task<int(int,int)> task(sum);//封装函数
    std::future<int> f = task.get_future();//获得future
    task(1,1); //直接调用
    //std::thread t{std::move(task),5,5}; //线程方式调用
    //t.join();//need join or detach thread
    int result = f.get();//获得结果

    std::shared_future

    类似于future,但是future的get方法只能被调用一次,二次调用可能出现未定义的行为(不一定抛出异常或出错)。shared_future的二次调用可以保证一样并且如有一次,可以保证异常一样。
    std::shared_future f = std::async(sum,1,1).share();//需要使用share来获得。

    std::thread

    C++的线程对象,和Java的线程非常不同,C++中线程较轻量级,线程的创建和执行的消耗较小,但是同时C++中线程没有明确的状态信息。
    1. thread创建后,C++始终尝试在一个新线程中执行人物,如果失败返回std::system_error
    2. 没有线程运行结果的接口,无法获得执行结果。
    3. 如果有异常在thread中发生,并且没有被捕获,std::terminate将会被调用
    4. 只能调用join去结束线程,或者detach去转成后台线程执行。否则当thread被析构时,程序会被abort。
    5. 如果线程detach并且main函数执行结束,所以的detach线程会被终止.
    6. 线程只可以通过std::move转移,不可以复制。
    创建线程的方式

    thread t1(sum,1,1);//传入执行方法和方法所需参数
    t1.join(); // or t1.detach() 等待或转入后台执行
    class SumTask{
    public:
        int sum(int a,int b){return a+b;}
    }
    thread t2(&SumTask::sum,this,1,1);//第二参数需要为对象的指针
     //使用packaged_task创建(见packaged_task)
    

    std::this_thread 获得当前线程对象,从而获得线程的ID信息, 或使用 yield方法交出线程控制权,sleep_for让线程休眠一定时间

    并发的几个问题

    1. 没有同步的数据访问。脏读
    2. 写入未完成。比如long long value = 100;可能没有执行完成
    3. 语句重排序。编译优化可能会重排语句
    

    解决方法:原子操作和锁

    关于C++的volatile,只保证读取肯定是从主存中读取,没有Java中得特定的原子性和次序处理。

    std::mutex互斥锁

    C++中互斥锁lock之后必须调用unlock,但是考虑可能存在的复杂情况,unlock不一定会被正确调用。使用C++的RAII(Res Acquisition is init 资源获取即初始化),利用destructor做unlock处理。
    std::mutex mutex;
    ...
    {
    std::lock_guard<std::mutex> lg(mutex);//lock
    }//unlock when block end

    std::recursive_mutex 可重入互斥锁

    尝试获取锁
    std::mutex m;
    while(!m.try_lock()){//可能失败即使锁可用
    doSomethings();
    }
    std::lock_guard<std::mutex> lg(m,std::adopt_lock);
    //不会lock,但保证锁可以被释放

    多个锁

    std::mutex m1;
    std::mutex m2;
    {
        std::lock(m1,m2);//lock m1 and m2
        //init with adopt_lock(will not do lock)
        std::lock_guard<mutex>    lockM1(m1,std::adopt_lock);
        std::lock_guard<mutex> lockM2(m2,std::adopt_lock);
    }
    

    try多个锁(不能保证死锁处理)

    int idx = std::try_locK(m1,m2);
    if(idx<0){//both locks success return -1
        lock_guard<mutex> lg1(m1,adopt_lock);
        lock_guard<mutex> lg1(m1,adopt_lock);
    }else{///lock failed
    }
    

    std::unique_lock 更加灵活的锁处理机制

    std::unique_lock<mutex> lock(mutex,std::try_to_lock);
    if(lock){ //lock success
    }
    

    一定时间后尝试获取锁

    unique_lock<timed_mutex> lock(mutex,chrono::seconds(1));
    

    延迟获取锁

    {
        std::unique_lock<mutex> lock(mutex,std::defer_lock);
        lock.lock();
    }//unlock is auto when block end
    

    std::once_flag 和 std::call_once

    多线程下保证方法只会调用一次

    std::once_flag oc;
    std::call_once(oc,initialize);//if not initiialize
    or std::call_once(oc,[](){initialize()}); 
    

    condition variable(条件变量)

    类似于java中Object对象的wait和notify,但是可以通过lambda加入条件
    条件变量本质在一定条件下交出锁的控制权,并进入等待状态。

    std::mutex readyMutex;
    std:condition_variable readyCondVar;
    //wait
    std::unique_lock<mutex> ul(readyMutex) //首先要获得锁
    ...//干一些事情
    readyCondVar.wait(ul); //释放锁并等待通知到来
    

    //条件的方式
    //如果是isReady == false,那么进入等待状态,即释放锁,交出线程执行片,等待直到获得通知。
    //同时,即使获得通知,如果条件仍然不满足,继续等待

    readyCondVar.wait(ul,[]()->bool{return isReady;});
    

    上面等价于

    function<bool> checkIsReady = []->bool{return isReady};
    if(!checkIsReady()){//not ready
       wait(ul);
    }
    

    //notify直接调用condVar的notify_one()或notify_all

    readyConfVar.notify_one
    readyConfVar.notify_all
    

    当wait被通知到的时候,将结束等待状态,同时开始通过竞争尝试获得锁

    关于notify_one和notify_all的区别:和JAVA类似,notify_all告知所有的线程结束等待状态,尝试获得锁
    notify_once告知某个线程结束等待状态,尝试获得锁。不要想着notify_all就会让所有的线程进入执行状态,如果是互斥锁,那么还有可能需要获得锁才能进入执行状态。也就是说,如果是互斥锁,notify_all之后,当前10个线程,只有一个线程A获得锁并继续执行,其他线程进入等待锁的状态。如果A释放锁,那么其他就个线程会继续竞争锁,知道所有的线程都获得锁并执行结束。
    这里也可以看出使用条件变量的好处可以极大的减少不必要的锁竞争,增加系统的消耗,合理的使用条件的变量一定程度提高系统性能。
    对于joinable(即没有detach的线程),即使线程当前处于wait状态,也会被立即唤醒

    原子性(Atomics)

    C++中原子类型保证不会被重排序以及原子性的读取和写入操作

    #include <atomic>  //for atomic types
    std::atomic<bool> isReady;
    

    所有的trivial,integral或指针类型为模板参数。
    [trivial]类型:简单说就是无构造函数的类型,具体的解释可以上网查找
    原子类型

    {
       statements;
       data = 42
       isReady.store(true);//存储
    }
    

    原子类型的store操作可以保证store前面所有的内存操作都被其他线程可见,即写入主存储。

    while(isReady.load()){//加载存储
    if(data == 42){ ... } //will be true
    }
    

    原子类型的load操作可以保证接下来的内存操作都可以被其他科线程可见。即读主存储。
    happen-before原则:只要store之前的数据操作,如data = 42,在load之后,一定可见。
    这个保证是由C++的memory_order_seq_cst,即sequential consistent memory order。
    注意:默认的atomic的构造函数没有初始化锁,可以使用下面语句初始化
    std::atomic<bool> isReady{false};//C++11的方式初始化

    相关文章

      网友评论

        本文标题:C++11: 多线程简明指南

        本文链接:https://www.haomeiwen.com/subject/afkvhttx.html