3-线程同步

原文地址:https://xie-peiquan.gitee.io/tags/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/

何为线程同步?有时候我们需要规范线程的执行顺序,令独立线程上的行为同步。以下介绍C++标准库提供的线程同步工具。

1 信号量

#include <semaphore.h>
sem_t semaphore;
sem_init(&semaphore, 0, 0);
void data_preparation_thread()
{
    while(more_data_to_prepare()){
        ......
        sem_post(&semaphore);    		//信号量发送(增加)
    }
}
void data_processing_thread()   
{
    while(true){
        sem_wait(&semaphore);			//等待信号量(减小)
        ......
    }
}

2 条件变量

std::mutex mut;
std::queue<data_chunk> data_queue;---  ①
std::condition_variable data_cond;
void data_preparation_thread()            // 由线程乙运行
{
    while(more_data_to_prepare())
    {
        data_chunk const data=prepare_data();
        {
            std::lock_guard<std::mutex> lk(mut);
            data_queue.push(data);---}
        data_cond.notify_one();---}
}
void data_processing_thread()           // 由线程甲运行
{
    while(true)
    {
        std::unique_lock<std::mutex> lk(mut);---  ④
        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;
    }
}

以上,data_cond.wait() 建议配合 data_cond.notify_one() 一起用。notify_one() 通知条件变量,使wait() 从阻塞中被唤醒,重新尝试获取锁,再次查验条件。也就是 data_cond.wait() 并非一直检查条件,而是需要 notify_one() 触发。 当有多个线程阻塞等待时,用notify_all() .

另外,为什么这里要使用 std::unique_lock 呢?因为当条件不成立时,wait()要释放锁。而std::lock_guard 无法提供这种灵活性。

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

虽然std::queue 的操作是原子性的,但是并不代表线程安全,其接口还是存在固有的条件竞争。我们需要把front()和pop()合并成一个函数,这与栈容器的top()和pop()合并相似。另外,为了增加灵活性,这里提供pop的两个变体,try_pop()和wait_and_pop(),一个非阻塞一个阻塞。以下是类定义:

#include <memory>    ---  ①为使用std::shared_ptr而包含此头文件
template<typename T>
class threadsafe_queue
{
public:
    threadsafe_queue();
    threadsafe_queue(const threadsafe_queue&);
    threadsafe_queue& operator=(
        const threadsafe_queue&) = delete;---  ②为简化设计而禁止赋值操作
    void push(T new_value);
    bool try_pop(T& value);---  ③
    std::shared_ptr<T> try_pop();---void wait_and_pop(T& value);
    std::shared_ptr<T> wait_and_pop();
    bool empty() const;
};

完整接口实现如下:

#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
    mutable std::mutex mut;---  ①互斥必须用mutable修饰(针对const对象,准许其数据成员发生变动)
    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();
        return true;
    }
    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();
    }
};

3 future机制

如果某个线程按计划只需等待一次,那么利用 future 等待更合适。标准库中有std::future 和 std::shared_future,其参照了std::unique_ptr 和 std::shared_ptr. 注意,虽然future对象能用于线程间通信,但是future对象不提供同步访问。若有多个线程访问同一个future对象,必须用互斥访问。

3.1 async 异步运行

使用 std::async 启动一个异步任务。与 std::thread 对象等待运行方式的不同, std::async 会返回一个 std::future 对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用 future 对象的get()方法阻塞获取。

std::async是C++中一种快速建立并行的方法,类似的,python中ProcessPoolExecutor也可以快速并行。

#include <future>
#include <iostream>
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
    std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
    do_other_stuff();
    std::cout<<"The answer is "<<the_answer.get()<<std::endl;   //阻塞获取
}

std::async 的传参与 std::thread的传参一致,若传入的是普通函数,则第一个参数是函数对象,其余为函数的入参;若传入的是类成员函数,则第一个参数为类成员函数地址,第二个参数是对象地址,其余为函数入参。注意如果要传引用,用std::ref()包装。

std::async 有两种运行方式,一种是起新线程的异步运行方法,一种是在当前线程的同步运行方法。通过设定 std::async 的第一个参数,可以指定运行方式。该参数值可以是 std::launch::deferred 或 std::launch::async,前者指定在本线程上延迟执行函数,后者则新起一个线程执行。注:若不设定,则由系统自行选择。

3.2 packaged_task 打包任务

std::packaged_task<>可打包函数对象和future对象,他可以作为线程池的构件单元。std::packaged_task<>是一个类模板,其模板参数是函数签名,如:void()表示一个函数,无入参,无返回值。std::packaged_task<>对象具有 get_futrue()方法,能够返回future对象。std::packaged_task 是个可调用对象,调用时函数开始运行。

std::packaged_task 这种特性使得可以在线程间传递任务,即当前线程将任务包装在 std::packaged_task 中,获得对应 future 对象后,传递给另外的线程,并由其触发任务运行。等需要用到结果时,再用future 对象获取。以下代码是GUI前端向后台线程推处理任务,其将用户的请求(点击事件等)传递给后台线程。

#include <deque>
#include <mutex>
#include <future>
#include <thread>
#include <utility>

std::mutex m;
std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread()---{
    while(!gui_shutdown_message_received())---{
        get_and_process_gui_message();---  ③
        std::packaged_task<void()> task;
        {
            std::lock_guard<std::mutex> lk(m);
            if(tasks.empty())---continue;
            task=std::move(tasks.front());---  ⑤
            tasks.pop_front();
        }
        task();---}
}
std::thread gui_bg_thread(gui_thread);
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
    std::packaged_task<void()> task(f);---  ⑦
    std::future<void> res=task.get_future();---  ⑧
    std::lock_guard<std::mutex> lk(m);
    tasks.push_back(std::move(task));---return res;---}

3.3 promise 等待设定值

std::promise 配合 std::futrue 可实现以下机制:等待数据的线程在 futrue 上阻塞,提供数据的线程则利用 promise 设定关联值,使futrue就绪。

#include <iostream>
#include <thread>
#include <future>

void initiazer(std::promise<int> * promObj)
{
    std::cout<<"新线程内部"<<std::endl;
    promObj->set_value(35);
}

int main()
{
    std::promise<int> promiseObj;
    std::future<int> futureObj = promiseObj.get_future();
    std::thread th(initiazer, &promiseObj);
    std::cout<<futureObj.get()<<std::endl;
    th.join();
    return 0;
}

3.4 shared_future 共享期待

std::future只有一个实例可以获得特定的同步结果,而 std::shared_future 实例是可拷贝的,所以多个对象可以引用同一关联“期望”的结果。

std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future());---  ①隐式转移归属权

3.5 等待多个futrue

当线程需要汇总各个std::async任务的处理结果时,需要逐个等待future对象,可能写出如下代码:

std::future<FinalResult> process_data(std::vector<MyData>& vec)
{
    size_t const chunk_size=whatever;
    std::vector<std::future<ChunkResult>> results;
    for(auto begin=vec.begin(),end=vec.end();beg!=end;){
        size_t const remaining_size=end-begin;
        size_t const this_chunk_size=std::min(remaining_size,chunk_size);
        results.push_back(
            std::async(process_chunk,begin,begin+this_chunk_size));
        begin+=this_chunk_size;
    }
    return std::async([all_results=std::move(results)](){
        std::vector<ChunkResult> v;
        v.reserve(all_results.size());
        for(auto& f: all_results)
        {
             v.push_back(f.get());---}
        return gather_results(v);
    });
}

当有任务完成时,代码①被唤醒,随之又进入休眠,等待下一个任务唤醒。我们有更简洁的写法,可以减少这种切换开销。采用 std::experimental::when_all 等待所有任务结束再唤醒。

std::experimental::future<FinalResult> process_data(
    std::vector<MyData>& vec)
{
    size_t const chunk_size=whatever;
    std::vector<std::experimental::future<ChunkResult>> results;
    for(auto begin=vec.begin(),end=vec.end();beg!=end;){
        size_t const remaining_size=end-begin;
        size_t const this_chunk_size=std::min(remaining_size,chunk_size);
        results.push_back(
            spawn_async(
            process_chunk,begin,begin+this_chunk_size));
        begin+=this_chunk_size;
    }
    return std::experimental::when_all(
        results.begin(),results.end()).then(---[](std::future<std::vector<
             std::experimental::future<ChunkResult>>> ready_results)
        {
            std::vector<std::experimental::future<ChunkResult>>
                all_results=ready_results .get();
            std::vector<ChunkResult> v;
            v.reserve(all_results.size());
            for(auto& f: all_results)
            {
                v.push_back(f.get());---}
            return gather_results(v);
        });
}
  • 32
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

>_ XPQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值