c++多线程(三)同步并发操作

来源:微信公众号「编程学习基地」

文章目录

      • 1等待一个事件或其他条件
        • 1.1等待条件达成
        • 1.2使用条件变量构建线程安全队列
      • 2使用期望等待一次性事件
        • 2.1带返回值的后台任务
        • 2.2std::async
        • 2.3任务与期望
        • 2.4使用std::promises
        • 2.5为“期望”存储“异常”
        • 2.6多个线程的等待
      • 3.限定等待时间
        • 3.1 时钟

在本章,将讨论如何使用条件变量等待事件,以及介绍期望,和如何使用它简化同步操作。

1等待一个事件或其他条件

当一个线程等待另一个线程完成任务时,它会有很多选择

1.持续的检查共享数据标志,查看等待线程是否结束

2.周期性间歇的检查共享数据标志,使用std::this_thread::sleep_for()

3.通过另一线程触发等待事件的机制,这种机制就称为“条件变量”(condition variable)。

1.1等待条件达成

C++标准库对条件变量有两套实现:std::condition_variablestd::condition_variable_any。这两个实现都包含在头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);

使用std::condition_variable处理数据等待

#include 
#include 
#include 
#include 
#include 
using namespace std;

bool more_data_to_prepare()
{
    return false;
}

struct data_chunk
{};

data_chunk prepare_data()
{
    return data_chunk();
}

void process(data_chunk&)
{}

bool is_last_chunk(data_chunk&)
{
    return true;
}

std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;

void data_preparation_thread()
{
    do
    {
        data_chunk const data=prepare_data();
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(data);
        data_cond.notify_one();
    }while(more_data_to_prepare());
    cout << "data_preparation_thread over..." <<endl;
}

void data_processing_thread()
{
    while(true)
    {
        cout << "data_processing_thread in..." <<endl;
        std::unique_lock<std::mutex> lk(mut);   //加锁,如果使用lock_guard那么在wait时mutex还是处于lock状态,只有在对象结束时才会unlock
        /**
         * @brief wait()会去检查lambda返回值
         *      1.lambda函数返回true时返回
         *      2.lambda函数返回false时,wait()函数将解锁互斥量,并且将这个线程置于阻塞或等待状态
         *      当另一个线程调用notify_one()通知条件变量时,处理数据的线程从睡眠状态中苏醒,重新获取互斥锁,并且对条件再次检查,在条件满足的情况下,
         *      从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁,并且重新开始等待。
         */
        data_cond.wait(lk,[]{return !data_queue.empty();}); 
        data_chunk data=data_queue.front();
        data_queue.pop();
        lk.unlock();
        process(data);
        if(is_last_chunk(data))
            break;
    }
    cout << "data_processing_thread over..." <<endl;
}

int main()
{
    std::thread t1(data_preparation_thread);
    std::thread t2(data_processing_thread);
    
    t1.join();
    t2.join();
}

std::condition_variable提供一个锁和一个lambda函数表达式(作为等待的条件)

wait()会去检查lambda返回值
* 1.lambda函数返回true时返回
* 2.lambda函数返回false时,wait()函数将解锁互斥量,并且将这个线程置于阻塞或等待状态
* 当另一个线程调用notify_one()通知条件变量时,处理数据的线程从睡眠状态中苏醒,重新获取互斥锁,并且对条件再次检查,在条件满足的情况下,
* 从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁,并且重新开始等待。

std::lock_guard没有std::unique_lock灵活,没有lock()、unlock()等接口。如果互斥量在线程休眠期间保持锁住状态,准备数据的线程将无法锁住互斥量,也无法添加数据到队列中;同样的,等待线程也永远不会知道条件何时满足。

1.2使用条件变量构建线程安全队列

线程安全的队列需要提供两个版本的try_pop()和wait_for_pop()

使用条件变量的线程安全队列(完整版)

#include 
#include 
#include 
#include 
#include 
using namespace std;

template<typename T>
class threadsafe_queue
{
private:
    mutable std::mutex mut;
    std::queue<T> data_queue;
    std::condition_variable data_cond;
public:
    threadsafe_queue()
    {}
    threadsafe_queue(threadsafe_queue const& other)
    {
        std::lock_guard<std::mutex> lk(other.mut);
        data_queue=other.data_queue;
    }

    void push(T new_value)
    {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(new_value);
        data_cond.notify_one();
    }

    void wait_and_pop(T& value)
    {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk,[this]{return !data_queue.empty();});
        value=data_queue.front();
        data_queue.pop();
    }

    std::shared_ptr<T> wait_and_pop()
    {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk,[this]{return !data_queue.empty();});
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }

    bool try_pop(T& value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty)
            return false;
        value=data_queue.front();
        data_queue.pop();
    }

    std::shared_ptr<T> try_pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty())
            return std::shared_ptr<T>();
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

int main()
{
    threadsafe_queue<int> queue;
    for (int i = 0; i < 3; i++)
    {
        queue.push(i+10);
    }
    while (!queue.empty())
    {
        std::shared_ptr<int> tmp = queue.try_pop();
        cout << "tmp:" << *tmp << endl;
    }
}

2使用期望等待一次性事件

当一个线程需要等待一个特定的一次性事件时,C++标准库模型将这种一次性事件称为“期望” (future)。

在等待任务期间它可以先执行另外一些任务,直到对应的任务触发,而后等待期望的状态会变为“就绪”(ready)。

在C++标准库中,有两种“期望”,使用两种类型模板实现,声明在头文件中: 唯一期望(unique futures)(std::future<>)和共享期望(shared futures)(std::shared_future<>)。

2.1带返回值的后台任务

当任务的结果你不着急要时,你可以使用std::async启动一个异步任务。与std::thread对象等待运行方式的不同,std::async会返回一个std::future对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才不会阻塞;

#include 
#include 
int find_the_answer_to_ltuae()
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 42;
}

void do_other_stuff()
{}

int main()
{
    std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
    do_other_stuff();
    std::future_status status;
    do {
        status = the_answer.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::cout<<"The answer is "<<the_answer.get()<<std::endl;
}

直接通过这个对象的get()成员函数获取值时会阻塞等待直到“期望”状态为就绪。

因为一个异步操作我们是不可能马上就获取操作结果的,只能在未来某个时候获取,但是我们可以以同步等待的方式来获取 结果,可以通过查询future的状态(future_status)来获取异步操作的结果。future_status有三种状态:

  • deferred:异步操作还没开始
  • ready:异步操作已经完成
  • timeout:异步操作超时

获取future结果有三种方式:get、wait、wait_for,其中get等待异步操作结束并返回结果,wait只是等待异步操作完成,没有返回值,wait_for是超时等待返回结果。

2.2std::async

当你使用std::async启动一个异步任务时,默认在调用async就开始创建线程

现在来看看std::async的原型async(std::launch::async | std::launch::deferred, f, args…),第一个参数是线程的创建策略,有两种策略,默认的策略是立即创建线程:

  • std::launch::async:在调用async就开始创建线程。
  • std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。

第二个参数是线程函数,第三个参数是线程函数的参数。

std::launch::deferred | std::launch::async表明实现可以选择这两种方式的一种。

使用std::async向函数传递参数

int getVal(int val){
    return val + 1;
}
std::future<int> tmp=std::async(getVal,10);
std::future<int> tmp2=std::async(std::launch::async,[](){
    return 10;
});

class Base{
public:
    void foo(int val,std::string const&str){
        std::cout<<"do an async operator.."<<std::endl;
    }
};
Base b;
auto tmp3 = std::async(&Base::foo,&b,42,"hello");

2.3任务与期望

std::packaged_task<>对一个函数或可调用对象,绑定一个期望。当std::packaged_task<> 对象被调用,它就会调用相关函数或可调用对象,将期望状态置为就绪,返回值也会被存储为相关数据。

std::packaged_task<int()> task([](){ 
        std::this_thread::sleep_for(std::chrono::seconds(3));
        return 7; 
		});
std::future<int> f1 = task.get_future();
std::thread t1(std::ref(task));
thread_guard guard(t1);
cout << "get future.." << endl;
auto r1 = f1.get();
cout << r1 << endl;

线程间传递任务

#include 
#include 
#include 
#include 
using namespace std;
std::mutex m;
std::deque<std::packaged_task<void()> > tasks;
static int threadStaue = 1;
void workThr(){
    while (threadStaue)
    {
        std::packaged_task<void()> task;
        {
            std::lock_guard<std::mutex> lk(m);
            if (tasks.empty()) // 4
                continue;
            task = std::move(tasks.front()); // 5
            tasks.pop_front();
        }
        task(); // 6
    }
}

template<typename Func>
std::future<void> post_task_for_thread(Func f)
{
  std::packaged_task<void()> task(f);  // 7
  std::future<void> res=task.get_future();  // 8
  std::lock_guard<std::mutex> lk(m);  // 9
  tasks.push_back(std::move(task));  // 10
  return res;
}
static int taskNum = 0;
int main(int argc, char* argv[])
{
    // 开启任务处理线程
    std::thread work(workThr);
    work.detach();
    //添加任务获取期望
    std::future<void> f = post_task_for_thread([](){
        std::this_thread::sleep_for(std::chrono::seconds(2));
        cout << "start task:" << ++taskNum << endl;
    });
    //等待期望就绪
    std::future_status status;
    do {
        status = f.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::this_thread::sleep_for(std::chrono::seconds(3));
    cout << "man over..." << endl;
}

这段代码在工作线程内循环处理任务。当队列中没有任务④,它将再次循环;除非,他能在队列中提取出一个任务⑤,然后释放队列上的锁,并且执行任务⑥。这里,“期望”与任务相关,当任务执行完成时,其状态会被置为“就绪”状态。

主线程内调用提供的函数⑦可以提供一个打包好的任务,可以通过这个任务⑧调用get_future()成员函数获取“期望”对象,并且在任务被推入列表⑨之前,“期望”将返回调用函数⑩。当需要知道线程执行完任务时,会等待“期望”改变状态;否则,则会丢弃这个“期望”。

2.4使用std::promises

std::promise为获取线程函数中的某个值提供便利,在线程函数中给外面传进来的promise赋值,当线程函数执行完成之后就可以通过promis获取该值了,值得注意的是取值是间接的通过promise内部提供的future来获取的。它的基本用法:

std::promise<int> pr;
std::thread t([](std::promise<int> &p){ 
    std::this_thread::sleep_for(std::chrono::seconds(3));
    p.set_value_at_thread_exit(9); },
    std::ref(pr));
thread_guard guard(t);
std::future<int> f = pr.get_future();
auto r = f.get();
cout << "val:" << r << endl;

2.5为“期望”存储“异常”

函数作为std::async的一部分时,当在调用时抛出一个异常,那么这个异常就会存储到“期望”的结果数据中,之后“期望”的状态被置为“就绪”,之后调用get()会抛出这个存储的异常。

当你将函数打包入std::packaged_task任务包中后,在这个任务被调用时,同样的事情也会发生;当打包函数抛出一个异常,这个异常将被存储在“期望”的结果中,准备在调用get()再次抛出。

下面的demo列举了std::packaged_taskstd::promise,std::future

#include 
#include 
#include 
#include 
using namespace std;
struct error_exc: std::exception
{
    const char* what() const throw(){
        return "error..";
    }
};
int main(int argc, char* argv[])
{
    #if 0
    std::packaged_task<int()> task([](){ 
        std::this_thread::sleep_for(std::chrono::seconds(3));
        //任务里面也直接抛异常
        throw error_exc();
        return 7; 
        });
    std::future<int> f1 = task.get_future(); 
    std::thread t1(std::ref(task)); 
    cout<<"get future.."<<endl;
    try{
        auto r1 = f1.get();
        cout << r1 << endl;
    }catch(const std::exception &e){
        cout<<"packaged_task:"<<e.what()<<endl;;
    }
    #else

    std::promise<int> pr;
    std::thread t1([](std::promise<int>& p){ 
        std::this_thread::sleep_for(std::chrono::seconds(1));
        try{
            // 捕获异常,不能直接抛异常
            throw error_exc();
        }
        catch (...){
            // 使用了std::current_exception()来检索抛出的异常
            p.set_exception(std::current_exception());
            // p.set_exception(std::copy_exception(error_exc()));   //可直接抛异常
        }
        },std::ref(pr));
    std::future<int> f = pr.get_future();
    try{
        auto r = f.get();
        cout << "val:"<< r <<endl;
    }
    catch (const std::exception &e){
        cout<<"promise:"<<e.what()<<endl;
    }
    #endif
    
    std::future<int> tmp=std::async(std::launch::async,[](){
        //直接抛异常
        throw error_exc();
        return 10;
    });
    try{
        auto r = tmp.get();
        cout << "val:"<< r <<endl;
    }
    catch (const std::exception &e){
        cout<<"async:"<<e.what()<<endl;
    }
    t1.join();
}

2.6多个线程的等待

在多个线程间等待一个期望,也可以称作共享期望

虽然std::future可以处理所有在线程间数据转移的必要同步,但是调用某一特殊std::future对象的成员函数,就会让这个线程的数据和其他线程的数据不同步。

std::future模型独享同步结果的所有权,并且通过调用get()函数,一次性的获取数据,这就让并发访问变的毫无意义——只有一个线程可以获取结果值,因为在第一次调用get()后,就没有值可以再获取了。

std::future是只移动的,所以其所有权可以在不同的实例中互相传递,而std::shared_future实例是可拷贝的,所以多个对象可以引用同一关联“期望”的结果。

#include 
#include 
#include 
#include 
using namespace std;

class thread_guard
{
    std::thread& t;
public:
    explicit thread_guard(std::thread& t_):
        t(t_)
    {}
    ~thread_guard()
    {
        if(t.joinable())
        {
            t.join();
        }
    }
    thread_guard(thread_guard const&)=delete;
    thread_guard& operator=(thread_guard const&)=delete;
};

int main(int argc, char* argv[])
{
    std::promise<int> pr;
    std::thread t([](std::promise<int>& p){ 
        std::this_thread::sleep_for(std::chrono::seconds(1));
        p.set_value_at_thread_exit(9);
        },std::ref(pr));
    thread_guard guard(t);
    std::shared_future<int> f = pr.get_future();
    std::thread t2([=]{     //shared_future提供拷贝,std::future只能移动,通过[&]捕获引用
        std::shared_future<int> f2 = f;
        cout<<"wait to get future in thread.."<<endl;
        auto r = f2.get();
        cout << "val:" << r << endl;
        });
    thread_guard guard2(t2);
    cout<<"wait to get future in main thread.."<<endl;
    auto r = f.get();
    cout << "val:"<< r <<endl;
}

通过std::promise对象的成员函数get_future()的返回值转移所以权是隐式的;用一个右值构造std::shared_future<>,得到std::future类型的实例。

std::future有一个share()成员函数,可用来创建新的std::shared_future ,并且可以直接转移“期望”的所有权。

std::shared_future<int> f = pr.get_future().share();

3.限定等待时间

之前介绍过的所有阻塞调用,将会阻塞一段不确定的时间,将线程挂起直到等待的事件发生。在很多情况下,这样的方式很不错,但是在其他一些情况下,你就需要限制一下线程等待的时间了。这允许你发送一些类似“我还存活”的信息,无论是对交互式用户,或是其他进程,亦或当用户放弃等待,你可以按下“取消”键直接终止等待。

介绍两种超时方式:一种是“时延”的超时方式,另一种是“绝对”超时方式。

第一种方式,需要指定一段时间(例如,30毫秒);

第二种方式,就是指定一个时间点(例如,协调世界时[UTC]17:30:15.045987023,2011年11月30日)。

多数等待函数提供变量,对两种超时方式进行处理。处理持续时间的变量以“_for”作为后缀,处理绝对时间的变量以"_until"作为后缀。

3.1 时钟

对于C++标准库来说,时钟就是时间信息源。特别是,时钟是一个类,提供了四种不同的信息:

  • 现在时间
  • 时间类型
  • 时钟节拍
  • 通过时钟节拍的分布,判断时钟是否稳定

关于时间以后再研究…

你可能感兴趣的:(#,c++11,c++,开发语言,c++11)