9 高级线程管理
在之前的章节,我们通过直接创建std::thread对象来管理线程。有几处你已经看出,这是不可取的,因为之后你必须管理线程对象的生命期,以及确定适合该问题的线程数和当前的硬件,等等。理想的情况是,你可以最大程度的将代码分成可以并行执行的小块,把它们交给编译器和标准库,然后说:“把它们并行化以获得最佳性能”。
这些例子中的另一个常见问题就是,你可能使用了多个线程来解决一个问题,但是希望在某个条件达成时提早结束。这也许是因为结果已经确定了,或者是错误产生,甚至是因为用户指明要终止任务。无论什么原因,线程都需要被通知“请停止”请求,以便于放弃当前任务,清理资源,而且尽快完成。
在这一章,我们将讨论关于管理线程和任务的机制,从自动管理多个线程以及在线程间划分任务开始。
9.1 线程池
在许多公司,通常在办公室度过时间的员工有时需要访问客户或供应商或参加贸易展览或会议。虽然这些外出或许很有必要,任何一天都可能有几个人要外出,甚至有些员工需要外出几个月甚至几年。让每个员工都拥有一辆公车是昂贵的也是不切实际的,通常公司会提供有限数量的汽车供所有员工使用。如果一个员工需要外出,那就在适当的时间预订一辆汽车,当它回到办公室时再交还回去以便其他员工使用。如果当前没有可用的汽车,那员工就不得不将外出时间向后调整了。
线程池的思想很类似。在大多数系统,为每一个任务分配一个线程是不现实的,但是您仍然希望尽可能利用可用的并发。线程池可以帮到你;可以将需要并行执行的任务提交给线程池,线程池将它们放到一个待执行的队列。然后工作线程会逐个将它们取出,然后执行他们,然后再取出下一个,不断循环。
构建一个线程池会遇到几个关键的设计问题,例如使用多少线程,以最有效的方式将任务分配给线程,以及是否可以等待一个任务完成。在这章,我们将看到一些线程池的实现是如何解决这些设计问题的,我们从最简单的线程池开始。
9.1.1 最简单的线程池
作为最简单的线程池,它的线城数是固定的(一般等于std::thread::hardware_concurrency())。当有工作要做时,你调用一个函数将任务放入待运行队列。每个工作线程从队列中取出任务,然后单独运行它,然后回到队列继续取出。在最简单的情况下,没有等待任务完成的方式。如果你需要的话,你必须自己管理同步。
下面列出这种线程池的一个简单实现:
Listing 9.1 Simple thread pool
template<typename T>
class thread_safe_queue;
class join_threads;
class thread_pool
{
std::atomic_bool done;
thread_safe_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)
}
};
这个实现拥有一个工作线程vector(2),用到了来自第6章的的线程安全队列(1)。这种情况下,用户不能等待任务,而且任务也不能返回任何值,所以你可以使用std::function<void()>来封装任务。submit()函数包装了函数或者一个可调用对象,使其成为一个 std::function<void()>实例,然后放入队列(12)。
线程在构造函数中开始:使用std::thread::hardware_concurrency()获取当前硬件支持的线程数(8)。成员函数 worker_thread()作为线程函数(9)。
如果抛出异常则线程创建就会失败,所以要保证已经开始的线程能够停止并正确清理。如果抛出异常,那么通过 try - catch块捕获,然后设置标志位done(10),伴随一个来自第8章的join_threads的实例,用于join所有线程。它也是在析构函数中工作的,析构时将done设置为true(11),然后join_threads就会确保所有线程在线程池销毁之前执行完毕。注意,成员变量的声明顺序很重要:标志位done和worker_queue必须在线程数组之前声明,而threads必须在joiner之前声明。这是为了确保成员变量按照正确的顺序销毁;例如如果线程还没有结束,那么你就不能安全的销毁队列。(同样,如果先销毁线程后销毁joiner,那么线程就不能join)
worker_thread()函数本身很简单:在循环中检测done(4),从队列中取出任务(5),并执行(6)。如果没有任务可以执行则调用std::this_thread::yield()休息一下(7),在下次循环取出之前给其他线程一个机会向队列中添加任务。
对于一般的事情来说,这样一个简单的线程池都可以满足,特别是在任务完全独立,而且不需要返回值,或者执行任何阻塞操作的情况下。但也有许多情况这个简单的线程池不能满足需求,甚至会导致诸如死锁等问题。而且,简单的情况下,使用std::async可能会比使用这个线程池更好(例如像第8章那样)。在这一章中,我们将研究更复杂的线程池的实现,它们具有更多的特性,满足用户需要,或者减少潜在的问题。首先解决任务等待问题。
9.1.2 等待提交给线程池的任务
在第8章的例子中,显示的创建线程,将任务划分给线程之后,宿主线程总是等待最新创建的线程完成,以确保在将结果返给调用者之前整个任务能够结束。那么对与线程池来说,你必须等待提交给线程池的任务,而不是等待工作线程本身。这类似与第8章中基于std::async的例子,它们等待future。对与9.1中的简单线程池来说,你必须手动使用第4章中的技术:条件变量或者future。这就增加了代码的复杂度;要是可以直接等待任务就好了。
通过把复杂的事务移到线程池内部,你就可以直接等待任务了。令submit()函数返回某种描述的句柄,然后可以使用这个句柄来等待任务完成。这个任务句柄应该包装条件变量或者future,那样会简化线程池的代码。
对于某些情况,主线程需要等待创建的任务完成,以获取任务计算的结果。在这本书中你已经见过这样的例子,例如第二章中的parallel_accumulate()。在这种情况下,可以使用future来合并结果传递的等待。下面的程序9.2显示了这种变化,它允许你等待任务完成并将结果从任务传递给等待线程。因为std::packaged_task<>实例不允许拷贝,只能移动,所以不能再给队列传递std::function<>实例,因为std::function<>要求保存的函数对象是可拷贝构造的。只能换一种方式,使用自定义的函数包装类来处理只能移动的类型。这是一个简单的函数调用操作的类型消除类。你只需要处理无参数无返回值的函数,所以,在实现中,它就一种直接的虚调用。
Listing 9.2 A thread pool with waitable tasks
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
{
thread_safe_queue<function_wrapper> work_queue;
void worker_thread()
{
while (!done)
{
function_wrapper task;
if (work_queue.try_pop(task))
{
task();
}
else
{
std::this_thread::yield();
}
}
}
public:
template<typename FunctionType>
std::future<typename std::result_of<FunctionType()>::type> submit(FunctionType f)//(1)
{
typedef typename std::result_of<FunctionType()>::type result_type;//(2)
std::packaged_task<result_type()> task(std::move(f));//(3)
std::future<result_type> res(task.get_future());//(4)
work_queue.push(std::move(task));//(5)
return res;//(6)
}
// rest as before
};
首先修改submit()函数(1),返回一个 std::future<>对象,允许等待任务结束并获取结果。这就需要你知道函数f的返回值类型,std::result_of<FunctionType()>::type代表调用FunctionType实例的返回值,它没有参数。然后使用它再定义一个result_type类型(2)。
然后将f包装到std::packaged_task<result_type()>中(3),因为f是一个无参数但返回 result_type类型的函数或者可调用对象。然后从std::packaged_task<>中获取future(4),然后将std::packaged_task<>放入队列(5),最后将future返回(6)。注意,必须使用 std::move()将任务放入队列,因为std: