Cpp Concurrency In Action(读书笔记8)——高级线程管理

10 篇文章 0 订阅
9 篇文章 0 订阅

线程池

  线程池:一个预先设定的线程组,会将任务指定给池中的线程。在大多数系统中,将每个任务指定给某个线程是不切实际的,不过可以利用现有的并发性,进行并发执行。线程池就提供了这样的功能,提交到线程池中的任务将并发执行,提交的任务将会挂在任务队列上。队列中的每一个任务都会被池中的工作线程所获取,当任务执行完成后,再回到线程池中获取下一个任务。

最简单的线程池

  作为最简单的线程池,其拥有固定数量的工作线程(通常工作线程数量与 std::thread::hardware_concurrency() 相同)。当工作需要完成时,可以调用函数将任务挂在任务队列中。每个工作线程都会从任务队列上获取任务,然后执行这个任务,执行完成后再回来获取新的任务。在最简单的线程池中,线程就不需要等待其他线程完成对应任务了。如果需要等待,就需要对同步进行管理。
  简单的线程池:

#include <atomic>
#include "join_threads.h"
#include "threadsafe_queue.h"
class thread_pool
{
    //注意成员声明顺序,使得以正确的顺序销毁
    std::atomic_bool done;
    threadsafe_queue<std::function<void()> > work_queue; // 1 线程安全队列管理任务队列
    std::vector<std::thread> threads; // 2 一组工作线程
    join_threads joiner; // 3 汇聚线程
    void worker_thread()
    {
        while (!done) // 4
        {
            std::function<void()> task;
            if (work_queue.try_pop(task)) // 5 从任务队列上获取队列
            {
                task(); // 6 执行任务
            }
            else
            {
                std::this_thread::yield(); // 7 没有任务,休息,给别的线程运行
            }
        }
    }
public:
    thread_pool() :
        done(false), joiner(threads)
    {
        unsigned const thread_count = std::thread::hardware_concurrency(); // 8 硬件支持线程数
        try
        {
            for (unsigned i = 0;i<thread_count;++i)
            {
                threads.push_back(
                    std::thread(&thread_pool::worker_thread, this)); // 9 线程执行初始函数
            }
        }
        catch (...)
        {
            done = true; // 10 设置标志
            throw;
        }
    }
    ~thread_pool()
    {
        done = true; // 11 此处仅需要设置标志,结束循环
    }
    template<typename FunctionType>
    void submit(FunctionType f)
    {
        work_queue.push(std::function<void()>(f)); // 12 包装函数,压入队列
    }
};

等待提交到线程池中的任务

  对简单线程池的修改,通过修改就能等待任务完成,以及在工作线程完成后,返回一个结果到等待线程中去,不过 std::packaged_task<> 实例是不可拷贝的,仅是可移动的,所以不能再使用 std::function<> 来实现任务队列,因为std::function<> 需要存储可复制构造的函数对象。包装一个自定义函数,用来处理只可移动的类型。这就是一个带有函数操作符的类型擦除类。只需要处理那些没有函数和无返回的函数,所以这是一个简单的虚函数调用。
  可等待任务的线程池:

#ifndef THREAD_POOL_H//.h
#define THREAD_POOL_H
#include <atomic>
#include <future>
#include "join_threads.h"
#include "threadsafe_queue.h"
class function_wrapper//带有函数操作符的类型擦除类
{//用来处理只可以移动的类型
    struct impl_base {
        virtual void call() = 0;//纯虚函数
        virtual ~impl_base() {}
    };
    std::unique_ptr<impl_base> impl;
    template<typename F>
    struct impl_type : impl_base
    {
        F f;
        impl_type(F&& f_) : f(std::move(f_)) {}
        void call() { f(); }
    };
public:
    template<typename F>
    function_wrapper(F&& f) :
        impl(new impl_type<F>(std::move(f)))
    {}
    void operator()() { impl->call(); }
    function_wrapper() = default;
    function_wrapper(function_wrapper&& other) :
        impl(std::move(other.impl))
    {}
    function_wrapper& operator=(function_wrapper&& other)
    {
        impl = std::move(other.impl);
        return *this;
    }
    function_wrapper(const function_wrapper&) = delete;
//?    function_wrapper(function_wrapper&) = delete;
    function_wrapper& operator=(const function_wrapper&) = delete;
};
class thread_pool
{
    std::atomic_bool done;
    threadsafe_queue<function_wrapper> work_queue;
    // 使用function_wrapper,而非使用std::function(需要存储可复制构造的函数对象)
    std::vector<std::thread> threads; // 一组工作线程
    join_threads joiner; // 汇聚线程
    void worker_thread()
    {
        while (!done)
        {
            function_wrapper task;
            if (work_queue.try_pop(task))
            {
                task();
            }
            else
            {
                std::this_thread::yield();
            }
        }
    }
public:
    thread_pool() :
        done(false), joiner(threads)
    {
        unsigned const thread_count = std::thread::hardware_concurrency(); //硬件支持线程数
        try
        {
            for (unsigned i = 0;i<thread_count;++i)
            {
                threads.push_back(
                    std::thread(&thread_pool::worker_thread, this)); //线程执行初始函数
            }
        }
        catch (...)
        {
            done = true; //设置标志
            throw;
        }
    }
    ~thread_pool()
    {
        done = true; //此处仅需要设置标志,结束循环
    }
    // 1 返回一个 std::future<> 保存任务的返回值,并且允许调用者等待任务完全结束。
    template<typename FunctionType>
    std::future<typename std::result_of<FunctionType()>::type>
        submit(FunctionType f)
    {
        typedef typename std::result_of<FunctionType()>::type
            result_type;
        std::packaged_task<result_type()> task(std::move(f));
        std::future<result_type> res(task.get_future());//获取future
        work_queue.push(std::move(task));//向任务队列推送任务,移动
        return res;
    }
};
#endif // !THREAD_POOL_H
//cpp
#include "thread_pool.h"
#include <iostream>
#include <numeric>
template<typename Iterator, typename T>
struct accumulate_block
{
    T operator()(Iterator first, Iterator last) // 1
    {
        return std::accumulate(first, last, T()); // 2 显示传入默认构造函数
    }
};
template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
    unsigned long const length = std::distance(first, last);
    if (!length)
        return init;
    unsigned long const block_size = 25;
    unsigned long const num_blocks = (length + block_size - 1) / block_size; 
    // 1 工作量是依据使用的块数,而不是线程的数量
    std::vector<std::future<T> > futures(num_blocks - 1);
    thread_pool pool;
    Iterator block_start = first;
    for (unsigned long i = 0;i<(num_blocks - 1);++i)
    {
        Iterator block_end = block_start;
        std::advance(block_end, block_size);
        futures[i] = pool.submit(//2 绑定的使用
            std::bind(accumulate_block<Iterator, T>(),
            std::move(block_start), std::move(block_end)));
        block_start = block_end;
    }
    T last_result = accumulate_block<Iterator, T>()(block_start, last);
    T result = init;
    for (unsigned long i = 0;i<(num_blocks - 1);++i)
    {
        result += futures[i].get();
    }
    result += last_result;
    return result;
}
int main()
{
    std::vector<int> v(200, 5);
    auto r=parallel_accumulate(v.begin(), v.end(),0);
    std::cout << r << std::endl;
    return 0;
}

  在2处的代码,原本不是这么写的(源码看不懂意思)。源代码在我vs2015(64bit)上编译错误(匹配不了submit函数)。几经波折,依然无效。暂且放下,看到下面快排的代码,点醒了我。翻开C++ primer,介绍std::bind的使用:用来绑定参数构建函数适配器。原来,参数是绑定的!
  线程池也需要注意异常安全。任何异常都会通过submit()返回给future,并在获取future的结果时,抛出异常。如果函数因为异常退出,线程池的析构函数会丢掉那些没有完成的任务,等待线程池中的工作线程完成工作。

等待依赖任务

  当等待某个数据块完成时,去处理未完成的数据块。如果使用线程池来管理任务列表和相关线程——使用线程池的主要原因——就不用再去访问任务列表了。最简单的方法就是在thread_pool中添加一个新函数,来执行任务队列上的任务,并对线程池进行管理。高级线程池的实现可能会在等待函数中添加逻辑,或等待其他函数来处理这个任务,优先的任务会让其他的任务进行等待。
基于线程池的快速排序实现:

//在thread_pool中添加成员函数
void run_pending_task()
{
    function_wrapper task;
    if (work_queue.try_pop(task))
    {
        task();
    }
    else
    {
        std::this_thread::yield();
    }
}
//cpp
#include "thread_pool.h"
#include <iostream>
#include <list>
template<typename T>
struct sorter // 1
{
    thread_pool pool; // 2
    std::list<T> do_sort(std::list<T>& chunk_data)
    {
        if (chunk_data.empty())
        {
            return chunk_data;
        }
        std::list<T> result;
        result.splice(result.begin(), chunk_data, chunk_data.begin());
        T const& partition_val = *result.begin();
        typename std::list<T>::iterator divide_point =
            std::partition(chunk_data.begin(), chunk_data.end(),
                [&](T const& val) {return val<partition_val;});
        std::list<T> new_lower_chunk;
        new_lower_chunk.splice(new_lower_chunk.end(),
            chunk_data, chunk_data.begin(),
            divide_point);
        std::future<std::list<T> > new_lower = // 3 在线程等待的时候,就会少向线程池中提交一个任务
            pool.submit(std::bind(&sorter::do_sort, this,//绑定,排序
                std::move(new_lower_chunk)));
        std::list<T> new_higher(do_sort(chunk_data));
        result.splice(result.end(), new_higher);
        while (new_lower.wait_for(std::chrono::seconds(0))//!
            ==std::future_status::timeout)
        {
            pool.run_pending_task(); // 4 执行任务队列上未完成的任务
        }
        result.splice(result.begin(), new_lower.get());
        return result;
    }
};
template<typename T>
std::list<T> parallel_quick_sort(std::list<T> input)
{
    if (input.empty())
    {
        return input;
    }
    sorter<T> s;
    return s.do_sort(input);
}
int main()
{
    std::list<int> l{ 35,3,4,44,66,22,11,222,333,55,1,0,9,6,
        35,3,4,44,66,22,11,222,333,55,1,0,9,6 };
    auto r = parallel_quick_sort(l);//9ms
    for(const auto &im:r)    std::cout<<im <<std::endl;
    system("pause");
    return 0;
}

  需要显式的管理线程和栈上要排序的数据块。每次对submit()的调用和对run_pending_task()的调用,访问的都是同一个队列。先前我们已经对此问题做过讨论(读书笔记7),存在乒乓缓存和数据争用问题。

避免队列中的任务竞争

  使用无锁队列会让任务没有明显的等待,但是乒乓缓存会消耗大量的时间。为了避免乒乓缓存,每个线程建立独立的任务队列。这样,每个线程就会将新任务放在自己的任务队列上,并且当线程上的任务队列没有任务时,去全局的任务列表中取任务。
  线程池——线程具有本地任务队列:

class function_wrapper;
class thread_pool
{
    std::atomic_bool done;
    std::vector<std::thread> threads; // 一组工作线程
    join_threads joiner; // 汇聚线程
    threadsafe_queue<function_wrapper> pool_work_queue;//全局队列
    typedef std::queue<function_wrapper> local_queue_type; // 1 普通队列,只有一个线程访问
    static thread_local std::unique_ptr<local_queue_type> local_work_queue; 
    // 2 不希望非线程池中的线程也拥有一个任务队列,up指向线程本地的工作队列
    void worker_thread()
    {
        local_work_queue.reset(new local_queue_type); // 3 up指针初始化,up析构,队列销毁
        while (!done)
        {
            run_pending_task();
        }
    }
public:
    thread_pool() :
        done(false), joiner(threads)
    {
        unsigned const thread_count = std::thread::hardware_concurrency(); //硬件支持线程数
        try
        {
            for (unsigned i = 0;i<thread_count;++i)
            {
                threads.push_back(
                    std::thread(&thread_pool::worker_thread, this)); //线程执行初始函数
            }
        }
        catch (...)
        {
            done = true; //设置标志
            throw;
        }
    }
    ~thread_pool()
    {
        done = true; //此处仅需要设置标志,结束循环
    }
    template<typename FunctionType>
    std::future<typename std::result_of<FunctionType()>::type>
        submit(FunctionType f)
    {
        typedef typename std::result_of<FunctionType()>::type result_type;
        std::packaged_task<result_type()> task(f);
        std::future<result_type> res(task.get_future());
        if (local_work_queue) // 4 检查当前线程是否具有一个工作队列
        {
            local_work_queue->push(std::move(task));//加入本地线程任务队列
        }
        else
        {
            pool_work_queue.push(std::move(task)); // 5 放入全局队列
        }
        return res;
    }
    void run_pending_task()
    {
        function_wrapper task;
        // 6 检查当前线程是否具有一个工作队列,该队列是否有任务
        if (local_work_queue && !local_work_queue->empty()) 
        {
            task = std::move(local_work_queue->front());
            local_work_queue->pop();
            task();
        }
        else if (pool_work_queue.try_pop(task)) // 7 从全局队列获取任务
        {
            task();
        }
        else
        {
            std::this_thread::yield();
        }
    }
};

  这样就能有效避免竞争,不过当任务分配不均时,造成的结果就是:某个线程本地队列中有很多任务的同时,其他线程无所事事。例如:举一个快速排序的例子,只有一开始的数据块能在线程池上被处理,因为剩余部分会放在工作线程的本地队列上进行处理,这样的使用方式也违背使用线程池的初衷。

窃取任务

  为了让没有任务的线程能从其他线程的任务队列中获取任务,就需要本地任务列表可以进行访问,这样才能让run_pending_tasks()窃取任务。需要每个线程在线程池队列上进行注册,或由线程池指定一个线程。同样,还需要保证数据队列中的任务适当的被同步和保护,这样队列的不变量就不会被破坏。实现一个无锁队列,让其拥有线程在其他线程窃取任务的时候,能够推送和弹出一个任务是可能的;不过,这个队列的实现就超出了本书的讨论范围。为了证明这种方法的可行性,将使用一个互斥量来保护队列中的数据。我们希望任务窃取是一个不常见的现象,这样就会减少对互斥量的竞争,并且使得简单队列的开销最小。下面,实现了一个简单的基于锁的任务窃取队列。
  基于锁的任务窃取队列:

#ifndef WORK_STEALING_QUEUE_H
#define WORK_STEALING_QUEUE_H
#include "function_wrapper.h"
class work_stealing_queue
{
private:
    typedef function_wrapper data_type;//类型擦除
    std::deque<data_type> the_queue; // 1 简单包装function_wrapper
    mutable std::mutex the_mutex;
public:
    work_stealing_queue()
    {}
    work_stealing_queue(const work_stealing_queue& other) = delete;
    work_stealing_queue& operator=(
        const work_stealing_queue& other) = delete;
    void push(data_type data) // 2 操作队列前端
    {
        std::lock_guard<std::mutex> lock(the_mutex);
        the_queue.push_front(std::move(data));
    }
    bool empty() const
    {
        std::lock_guard<std::mutex> lock(the_mutex);
        return the_queue.empty();
    }
    bool try_pop(data_type& res) // 3 操作队列前端
    {
        std::lock_guard<std::mutex> lock(the_mutex);
        if (the_queue.empty())
        {
            return false;
        }
        res = std::move(the_queue.front());
        the_queue.pop_front();
        return true;
    }
    bool try_steal(data_type& res) // 4 操作队列后端
    {
        std::lock_guard<std::mutex> lock(the_mutex);
        if (the_queue.empty())
        {
            return false;
        }
        res = std::move(the_queue.back());
        the_queue.pop_back();
        return true;
    }
};
#endif // !WORK_STEALING_QUEUE_H

  这就说明每个线程中的“队列”是一个后进先出的栈,最新推入的任务将会第一个执行。从缓存角度来看,这将对性能有所提升,因为任务相关的数据一直存于缓存中,要比提前将任务相关数据推送到栈上好。
  使用任务窃取的线程池:

#ifndef THREAD_POOL_STEAL_H
#define THREAD_POOL_STEAL_H
#include "function_wrapper.h"
#include "threadsafe_queue.h"
#include "work_stealing_queue.h"
#include "join_threads.h"
class thread_pool
{
    typedef function_wrapper task_type;
    std::atomic_bool done;
    threadsafe_queue<task_type> pool_work_queue;
    std::vector<std::unique_ptr<work_stealing_queue> > queues;
    // 1 每个线程自己的工作队列将存储在线程池的全局工作队列中
    std::vector<std::thread> threads;
    join_threads joiner;
    static thread_local work_stealing_queue* local_work_queue; 
    // 2 每个线程都有work_stealing_queue,后入先出
    static thread_local unsigned my_index;
    void worker_thread(unsigned my_index_)
    {
        my_index = my_index_;
        local_work_queue = queues[my_index].get(); 
        // 3 列表中队列的序号,会传递给线程函数,然后使用序号来索引对应队列
        while (!done)
        {
            run_pending_task();
        }
    }
    bool pop_task_from_local_queue(task_type& task)
    {
        return local_work_queue && local_work_queue->try_pop(task);
    }
    bool pop_task_from_pool_queue(task_type& task)
    {
        return pool_work_queue.try_pop(task);
    }
    bool pop_task_from_other_thread_queue(task_type& task) // 4
    {
        /*为了避免每个线程都尝试从列表中的第一个线程上窃取任务,
        *每一个线程都会从下一个线程开始遍历,
        *通过自身的线程序号来确定开始遍历的线程序号。
        */
        for (unsigned i = 0;i<queues.size();++i)
        {
            unsigned const index = (my_index + i + 1) % queues.size(); // 5
            if (queues[index]->try_steal(task))
            {
                return true;
            }
        }
        return false;
    }
public:
    thread_pool() :
        done(false), joiner(threads)
    {
        unsigned const thread_count = std::thread::hardware_concurrency();
        try
        {
            for (unsigned i = 0;i<thread_count;++i)
            {
                // 6 当每个线程被创建,就创建了一个属于自己的工作队列
                queues.push_back(std::unique_ptr<work_stealing_queue>( 
                    new work_stealing_queue));
                threads.push_back(
                    std::thread(&thread_pool::worker_thread, this, i));//序号传入
            }
        }
        catch (...)
        {
            done = true;
            throw;
        }
    }
    ~thread_pool()
    {
        done = true;
    }
    template<typename FunctionType>
    std::future<typename std::result_of<FunctionType()>::type> 
        submit(FunctionType f)
    {
        typedef typename std::result_of<FunctionType()>::type result_type;
        std::packaged_task<result_type()> task(f);
        std::future<result_type> res(task.get_future());
        if (local_work_queue)
        {
            local_work_queue->push(std::move(task));
        }
        else
        {
            pool_work_queue.push(std::move(task));
        }
        return res;
    }
    void run_pending_task()
    {
        task_type task;
        if (pop_task_from_local_queue(task) || // 7 从线程的任务队列中取出一个任务来执行
            pop_task_from_pool_queue(task) || // 8 或从线程池队列中获取一个任务
            pop_task_from_other_thread_queue(task)) 
            // 9 亦或从其他线程的队列中获取一个任务(会遍历池中所有线程的任务队列,然后尝试窃取任务)
        {
            task();
        }
        else
        {
            std::this_thread::yield();
        }
    }
};
#endif // !THREAD_POOL_STEAL_H

  这就意味着线程池可以访问任意线程中的队列,给闲置线程窃取任务。

中断线程

  需要使用信号来让未结束线程停止运行。这里需要一种合适的方式让线程主动的停下来,而非让线程戛然而止。

启动和中断线程

  interruptible_thread的基本实现:

#ifndef INTERRUPTIBLE_THREAD_H
#define INTERRUPTIBLE_THREAD_H
#include <thread>
class interrupt_flag//后面实现
{
public:
    void set();
    bool is_set() const;
};
thread_local interrupt_flag this_thread_interrupt_flag; // 1
class interruptible_thread
{
    std::thread internal_thread;
    interrupt_flag* flag;
public:
    template<typename FunctionType>
    interruptible_thread(FunctionType f)
    {
        std::promise<interrupt_flag*> p; // 2 线程将会持有f副本和本地promise变量(p)的引用
        internal_thread = std::thread([f, &p] { // 3 提供函数f是包装了一个lambda函数
            p.set_value(&this_thread_interrupt_flag);
            f(); // 4 让线程能够调用提供函数的副本
        });
        //lambda函数设置promise变量的值到this_thread_interrupt_flag(在thread_local中声明)的地址中
        flag = p.get_future().get(); // 5 等待就绪,结果存入到flag成员变量中
    }
    void interrupt()//需要一个线程去做中断时,需要一个合法指针作为一个中断标志
    {
        if (flag)
        {
            flag->set(); // 6 仅对标志进行设置
        }
    }
};
#endif // !INTERRUPTIBLE_THREAD_H

  注意,即使lambda函数在新线程上执行,对本地变量p进行悬空引用,都没有问题,因为在新线程返回之前,interruptible_thread构造函数会等待变量p,直到变量p不被引用。实现没有考虑处理汇入线程,或分离线程。所以,需要flag变量在线程退出或分离前已经声明,这样就能避免悬空问题。

检查线程是否中断

  添加成员函数:

void interruption_point()
{
    if (this_thread_interrupt_flag.is_set())
    {
        throw thread_interrupted();
    }
}

  使用:

void foo()
{
    while (!done)
    {
        interruption_point();
        process_next_item();
    }
}

  以上并不是一个优雅的设计。最好是在线程等待或阻塞的时候中断线程,因为这时的线程不能运行,也就不能调用interruption_point()函数!

中断等待——条件变量

  最简单的方式是,当设置中断标志时,需要提醒条件变量,并在等待后立即设置断点。为了让其工作,需要提醒所有等待对应条件变量的线程,就能确保感兴趣的线程能够苏醒。伪苏醒是无论如何都要处理的,所以其他线程(非感兴趣线程)将会被当作伪苏醒处理。
  std::condition_variable 实现的interruptible_wait有问题版:

void interruptible_wait(std::condition_variable& cv,
    std::unique_lock<std::mutex>& lk)
{
    interruption_point();
    this_thread_interrupt_flag.set_condition_variable(cv); 
    // 1 为当前线程关联条件变量
    cv.wait(lk); // 2 等待条件变量
    this_thread_interrupt_flag.clear_condition_variable(); 
    // 3 清理相关条件变量,并且再次检查中断
    interruption_point();
}

  如果线程在等待期间被条件变量所中断,中断线程将广播条件变量,并唤醒等待该条件变量的线程,所以这里就可以检查中断。
  不幸的是,代码有两个问题。第一个问题比较明显,如果想要线程安全: std::condition_variable::wait() 可以抛出异常,所以这里会直接退出,而没有通过条件变量删除相关的中断标志。这个问题很容易修复,就是在析构函数中添加相关删除操作即可。第二个问题就不大明显了,这段代码存在条件竞争。虽然,线程可以通过调用interruption_point()被中断,不过在调用wait()后,条件变量和相关中断标志就没有什么关系了,因为线程不是等待状态,所以不能通过条件变量的方式唤醒。就需要确保线程不会在最后一次中断检查和调用wait()间被唤醒。解决:使用lk上的互斥量对线程进行保护,这就需要将lk传递到set_condition_variable()函数中去。这将带来两个新问题,详细请查阅原书内容,链接在读书笔记1中。所以选择了另一种方案:放置超时等待。
   std::condition_variableinterruptible_wait中使用超时:

class interrupt_flag
{
    std::atomic<bool> flag;
    std::condition_variable* thread_cond;
    std::mutex set_clear_mutex;
public:
    interrupt_flag() :
        thread_cond(0)
    {}
    void set()
    {
        flag.store(true, std::memory_order_relaxed);
        std::lock_guard<std::mutex> lk(set_clear_mutex);
        if (thread_cond)
        {
            thread_cond->notify_all();
        }
    }
    bool is_set() const
    {
        return flag.load(std::memory_order_relaxed);
    }
    void set_condition_variable(std::condition_variable& cv)
    {
        std::lock_guard<std::mutex> lk(set_clear_mutex);
        thread_cond = &cv;
    }
    void clear_condition_variable()
    {
        std::lock_guard<std::mutex> lk(set_clear_mutex);
        thread_cond = 0;
    }
    struct clear_cv_on_destruct
    {
        ~clear_cv_on_destruct()
        {
            this_thread_interrupt_flag.clear_condition_variable();
        }
    };
};
//函数
void interruptible_wait(std::condition_variable& cv,
    std::unique_lock<std::mutex>& lk)
{
    interruption_point();
    this_thread_interrupt_flag.set_condition_variable(cv);
    interrupt_flag::clear_cv_on_destruct guard;
    interruption_point();
    cv.wait_for(lk, std::chrono::milliseconds(1));//放置超时等待
    interruption_point();
}

  如果有谓词(相关函数)进行等待,1ms的超时将会完全在谓词循环中完全隐藏:

template<typename Predicate>
void interruptible_wait(std::condition_variable& cv,
    std::unique_lock<std::mutex>& lk,
    Predicate pred)
{
    interruption_point();
    this_thread_interrupt_flag.set_condition_variable(cv);
    interrupt_flag::clear_cv_on_destruct guard;
    while (!this_thread_interrupt_flag.is_set() && !pred())
    {
        cv.wait_for(lk, std::chrono::milliseconds(1));
    }
    interruption_point();
}

使用 std::condition_variable_any 中断等待

  std::condition_variable_anystd::condition_variable的不同在于, std::condition_variable_any可以使用任意类型的锁,而不仅有 std::unique_lock<std::mutex> 。可以让事情做起来更加简单,并且 std::condition_variable_any 可以比 std::condition_variable 做的更好。因为能与任意类型的锁一起工作,就可以设计自己的锁,上锁/解锁interrupt_flag的内部互斥量set_clear_mutex,并且锁也支持等待调用。
  std::condition_variable_any 设计的interruptible_wait

class interrupt_flag
{
    std::atomic<bool> flag;
    std::condition_variable* thread_cond;
    std::condition_variable_any* thread_cond_any;
    std::mutex set_clear_mutex;
public:
    interrupt_flag() :
        thread_cond(0), thread_cond_any(0)
    {}
    void set()
    {
        flag.store(true, std::memory_order_relaxed);
        std::lock_guard<std::mutex> lk(set_clear_mutex);
        if (thread_cond)
        {
            thread_cond->notify_all();
        }
        else if (thread_cond_any)
        {
            thread_cond_any->notify_all();
        }
    }
    template<typename Lockable>
    void wait(std::condition_variable_any& cv, Lockable& lk)
    {
        struct custom_lock//自定义的锁
        {
            interrupt_flag* self;
            Lockable& lk;
            custom_lock(interrupt_flag* self_,
                std::condition_variable_any& cond,//cv传入
                Lockable& lk_) :
                self(self_), lk(lk_)
            {
                self->set_clear_mutex.lock();
                // 1 自定义的锁类型在构造的时候,需要所锁住内部set_clear_mutex
                self->thread_cond_any = &cond; 
                // 2 对thread_cond_any指针进行设置
                //引用std::condition_variable_any 传入锁的构造函数中
            }
            // 3 当条件变量调用自定义锁的unlock()函数中的wait()时,
            //就会对Lockable对象和set_clear_mutex(mutex)进行解锁
            void unlock()
            {
                lk.unlock();
                self->set_clear_mutex.unlock();
            }
            void lock()
            {
                std::lock(self->set_clear_mutex, lk); 
                /* 4 当wait()结束等待(因为等待,或因为伪苏醒),
                *因为线程将会调用lock()函数,
                *这里依旧要求锁住内部set_clear_mutex,并且锁住Lockable对象
                */
            }
            ~custom_lock()
            {
                self->thread_cond_any = 0; 
                /* 5 在wait()调用时,custom_lock的析构函数中
                *清理thread_cond_any指针(同样会解锁set_clear_mutex)之前,
                *可以再次对中断进行检查。
                */
                self->set_clear_mutex.unlock();
            }
        };
        custom_lock cl(this, cv, lk);//自定义锁,cv可以与其它锁一起工作
        interruption_point();
        cv.wait(cl);
        interruption_point();
    }
    // 其余和上面一样
};
//函数
template<typename Lockable>
void interruptible_wait(std::condition_variable_any& cv,
    Lockable& lk)
{
    this_thread_interrupt_flag.wait(cv, lk);
}

  这就允许线程可以尝试中断其他线程获取set_clear_mutex锁;以及在内部wait()调用之后,检查thread_cond_any指针。这就是在替换 std::condition_variable 后,所拥有的功能(不包括管理)。

中断其他阻塞调用

  通常情况下,可以使用 std::condition_variable 的超时选项,因为在实际运行中不可能很快的将条件变量的等待终止(不访问内部互斥量或future的话)。不过,在某些情况下,你知道你在等待什么,这样就可以让循环在interruptible_wait()函数中运行。作为一个例子,这里为 std::future<> 重载了interruptible_wait()的实现:

template<typename T>
void interruptible_wait(std::future<T>& uf)
{
    while (!this_thread_interrupt_flag.is_set())
    {
        if (uf.wait_for(lk, std::chrono::milliseconds(1) ==
            std::future_status::ready)
            break;
    }
    interruption_point();
}

  等待时间不接受时,如果这必要,且时钟支持的话,可以持续削减超时时间。这种方式将会让线程苏醒很多次,来检查标志,并且增加线程切换的开销。

处理中断

  捕获中断,进行处理。

try
{
    do_something();
}
catch (thread_interrupted&)
{
    handle_interruption();
}

  其他线程再次调用interrupt()时,线程将会再次被中断,这就被称为断点(interruption point)。
  异常将会终止独立进程,就能保证未处理的中断是异常安全的。interruptible_thread构造函数中对线程的初始化,实现如下:

internal_thread = std::thread([f, &p] {
    p.set_value(&this_thread_interrupt_flag);
    try
    {
        f();
    }
    catch (thread_interrupted const&)
    {
    }
});

应用退出时中断后台任务

  在后台监视文件系统:

std::mutex config_mutex;
std::vector<interruptible_thread> background_threads;
void background_thread(int disk_id)
{
    while (true)
    {
        interruption_point(); // 1 在循环中对中断进行检查
        fs_change fsc = get_fs_changes(disk_id); 
        // 2 后台线程运行在一个循环中,并时刻检查磁盘的变化
        if (fsc.has_changes())
        {
            update_index(fsc); // 3 对其序号进行更新
        }
    }
}
void start_background_processing()
{
    background_threads.push_back(
        interruptible_thread(background_thread, disk_1));
    background_threads.push_back(
        interruptible_thread(background_thread, disk_2));
}
int main()
{
    start_background_processing(); // 4 启动时,后台线程就已经启动
    process_gui_until_exit(); // 5 对应线程将会处理GUI
    std::unique_lock<std::mutex> lk(config_mutex);
    for (unsigned i = 0;i<background_threads.size();++i)
    {
        background_threads[i].interrupt(); // 6 后台进程将会被中断
    }
    for (unsigned i = 0;i<background_threads.size();++i)
    {
        background_threads[i].join(); 
        // 7 主线程会等待每一个后台线程结束后才退出
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值