本章主要关于线程和任务的管控机制,起步内容是自动控制线程数目,以及线程间自动划分任务。
目录
2.4中断std::condition_variable_any上的等待
1.线程池
在大多数系统上,若仅因为某任务可以与其他任务并行处理,就分别给它们配备单独的专属线程,这种做法不切实际,但我们依然想充分利用可调配的并发算力。线程池可以实现我们的目的:将可同时执行的任务提交到线程池,并放入任务队列中等待,队列中的任务分别由某一线程领取执行,执行完成后该线程再从队列中取出另一任务,如此反复循环。
线程池的构建涉及几个关键要素:线程数量、分配任务的方法、等待任务完成与否。
1.1简易线程池
最简单的实现形式是使用固定数目的工作线程(往往是std::hardware_concurrency())。每当有任务要处理,就调用某个函数,将它放入任务队列等待,工作线程从队列领取指定任务执行。简易的线程池无法等待任务。
class thread_pool
{
std::atomic_bool done;
threadsafe_queue<std::function<void()>> work_queue;
std::vector<std::thread> threads;
join_threads joiner;
void worker_thread()
{
while (!done)//只要线程池没有析构或抛出异常,就一直尝试从队列取出任务执行
{
std::function<void()> task;
if (work_queue.try_pop(task))
{
task();
}
else
{
std::this_thread::yield();//没有任务则稍事休息
}
}
}
public:
thread_pool() :done(false), joiner(threads)
{
const unsigned 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>
void submit(FunctionType f)
{
work_queue.push(std::function<void()>(f));
}
};
本例假设装入线程池的任务没有任何返回值,故将他封装为std::function<void()>实例,向submit()传入函数或可调用对象,都会包装在一个std::function<void()>实例中。
注意声明数据成员的顺序,done标志、work_queue、threads、joiner,为了确保数据成员按正确的顺序销毁:线程全部终结,任务列表才销毁。
上述线程池适合下列特点的任务:彼此完全独立、没有任何返回值、不执行阻塞操作。对于简单的问题,std::async()可能是更好的方式。
1.2支持等待任务的线程池
如果主线程需要获取某任务的结果,就必须等待生成的线程完成任务返回。运用future可将等待和传递结果两个操作合二为一。
由于std::packaged_task<>实例仅能移动不能复制,但std::function()要求本身所含的函数对象能进行拷贝构造,因此需要定制自己的类作为代替,包装任务以处理只移型别。
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;
thread_safe_queue<function_wrapper> work_queue;
std::vector<std::thread> threads;
join_threads joiner;
void worker_thread()
{
while (!done)
{
function_wrapper task;
if (work_queue.try_pop(task))
{
task();//调用call,进而调用函数
}
else
{
std::this_thread::yield();
}
}
}
public:
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));//packaged_task不可复制一定要使用std::move()
return res;
}
thread_pool() :done(false), joiner(threads)
{
const unsigned 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;
}
};
任务队列中存储function_wrapper而非std::function<>。
我们可以设置数据块,把处理每个数据块的任务放入线程池,数据块体积设置为值得并发处理的最小尺寸,以发挥线程池的伸缩性。但任务规模不能过小,因为std::future<>返回结果附带固有开销,过小的任务导致频繁的等待会得不偿失。
线程池还顾及了异常安全,如果任务抛出异常,会通过submit()返回的future向外传播,线程池的析构函数丢弃尚未完成的任务,等待池内线程自行结束。
1.3支持任务相互等待的线程池
快速排序算法:从元素中选出一个基准元素,整体数据分成两部分,小于基准元素的排到它前面,大于基准元素的排到它后面,两部分按照上述规则递归排序,最终拼接为有序序列。
回顾第8章,我们使用栈容器管理待排序的数据块,各线程进行局部排序,操作过程中再将数据分成两部分,一部分由当前线程排序,另一部分调用先压入栈,等有空余线程再对栈内数据块排序,若当前线程已经处理好自己那部分的数据块,则会协助另一线程(从栈内取出数据块排序)。
为了避免空闲线程耗尽令线程停滞不前,我们参考第8章的方法:在等待数据块完成操作的过程中,主动处理相关还未排序的数据块。
在线程池上增加一个新函数,负责运行队列中的任务,自行管控“领取任务并执行”的循环,取代原本的线程饱和循环:
void thread_pool::run_pending_task()
{
function_wrapper task;
if (work_queue.try_pop(task))
{
task();
}
else
{
std::this_thread::yield();
}
}
基于线程池的快速排序:
template<typename T>
struct sorter
{
thread_pool pool;
std::list<T> do_sort(std::list<T>& chunk_data)
{
if (chunk_data.empty())
{
return chunk_data;
}
std::list<T> res;
res.splice(res.begin(), chunk_data, chunk_data.begin());
const T& partition = *res.begin();
std::list<T>::iterator divide_point = std::partition(chunk_data.begin(), chunk_data.end(),
[&](const& T val) {return val < partition; });
//创建小于部分的链表
std::list<T> new_lower_chunk;
new_lower_chunk.splice(new_lower.end(),
chunk_data,
chunk_data.begin(), divide_point);
//std::bind绑定成员函数时,第一个参数是对象的成员函数,第二个参数需指名对象的地址
std::future<std::list<T>> new_lower = pool.submit(std::bind(&sorter::do_sort, this, std::move(new_lower_chunk)));
//此部分由线程池管理--------
//chunks.push(std::move(new_lower_chunk));//新划分出来的数据压入栈
//if (threads.size() < max_thread_count)//存在空闲线程时才分配新线程
//{
// threads.push_back(std::thread(&sorter<T>::sort_thread, this));//递归调用排序
//}
//-------------------------
//创建大于部分的链表
std::list<T> new_higher(do_sort(chunk_data));//递归调用排序
res.splice(res.end(), new_higher);
while (new_lower.wait_for(std::chrono::second(0)) == std::future_status::timeout)//有其它线程未就绪
{
pool.run_pending_task();//从线程池取出任务
}
res.splice(res.begin(), new_lower.get());
return res;
}
};
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);
}
尽管上述实现解决了任务相互等待导致死锁,但线程池还不理想。每个submit()和每个run_pending_task()的调用都访问相同的任务队列。若同一组数据由多个线程并发改动,会对性能造成不利影响。
1.4避免任务队列上的争夺
线程池具备一个任务队列供多个线程共用,每当有线程调用submit(),就把新任务压入该队列,为了执行任务,不断从这一队列弹出任务,队列上的争夺行为随着处理器数目的增多而加剧。
针对此问题,我们可以为每个池内线程配备局部的任务队列,只有当局部任务队列中没有任务时,再从全局队列中取出任务:
class thread_pool
{
std::atomic_bool done;
threadsafe_queue<function_wrapper> pool_work_queue;
typedef std::queue<function_wrapper> local_queue_type;
std::vector<std::thread> threads;
join_threads joiner;
//关键字对程序内所有线程均起作用,池外线程队列指针为空
static thread_local std::unique_ptr<local_queue_type> local_work_queue;
void worker_thread()//初始化局部队列
{
local_work_queue.reset(new local_queue_type);
while (!done)
{
run_pending_task();
}
}
public:
thread_pool() :done(false), joiner(threads)
{
const unsigned 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)//判别当前线程是否为池内线程
{
local_work_queue->push(std::move(task));
}
else
{
pool_work_queue.push(std::move(task));
}
return res;
}
void run_pending_task()
{
function_wrapper task;
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))//从全局队列中取出任务
{
task();
}
else
{
std::this_thread::yield();
}
}
};
线程局部变量thread_local
若将变量声明为线程局部变量,则每个线程上都会存在其独立的实例。
三种数据能声明为线程局部变量:
以名字空间为作用域、类的静态数据成员、普通局部变量。
⊙线程局部变量在给定翻译单元内(当前代码所在源文件,及预处理后全部有效包含的头文件或其它源文件)动态加载,若局部变量未被指涉,则无法保证它们已被构造出来。
⊙与静态变量一样,它们先进行零值初始化,再进行动态初始化(零值初始化和常量初始化以外的方式)。若其构造/析构函数抛出异常,则程序会调用std::terminate()。⊙线程局部变量的地址因不同线程而异,若线程通过std::exit()或main()(先获取main()返回值再调用std::exit())自然退出,则局部变量随之销毁。若应用程序退出,线程仍在运行,则线程上的局部变量不会发生析构。
局部线程队列由worker_thread初始化(worker_thread由构造函数调用),初始化过程会为池内线程创建thread_local声明的任务队列,再进入任务处理循环。
submit()判别当前线程是否具备任务队列,若具备则为池内线程,把任务压入;否则,将任务加入全局队列。
run_pending_task()通过判断,优先选择从局部线程取出任务,再从全局取出,或等待。
上述方法减少了争夺,但是容易导致分配不均,以快速排序为例,可能只将顶层的数据块任务放入全局队列,而剩下的数据块全都加到自身的局部队列中(递归调用)。
1.5任务窃取
假设某线程运行run_pending_task(),自身队列却没有任务,而另一线程队列则是满的,前者可以通过某种方式窃取后者的任务,以达到分配均匀的目的。
为了简单阐明任务窃取思想,采用互斥保护队列数据,以下是基于锁的队列的简单实现:
class work_stealing_queue
{
private:
typedef function_wrapper data_type;
std::queue<data_type> the_queue;
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)
{
std::lock_guard<std::mutex> lk(the_mutex);
the_queue.push_front(data);
}
bool empty()
{
std::lock_guard<std::mutex> lk(the_mutex);
return the_queue.empty();
}
bool try_pop(data_type& data)
{
std::lock_guard<std::mutex> lk(the_mutex);
data = std::move(the_queue.front());
the_queue.pop_front();
return true;
}
bool try_steal(data_type& data)
{
std::lock_guard<std::mutex> lk(the_mutex);
if (the_queue.empty())
{
return false;
}
data = std::move(the_queue.back());
the_queue.pop_back();
return true;
}
};
该队列中,push()和try_pop()都操作队列的前端,而try_steal()则操作队列的末端,实际上是一个先进先出的“队列”,从缓存角度来看,有助于线程的改进,因为队列优先处理最近压入的任务,而最近的任务数据更有可能还留在缓存中。try_steal()则从另一端获取任务,最大限度地避免数据争夺。
把该队列应用在线程池中:
class thread_pool
{
typedef function_wrapper task_type;
std::atomic_bool done;
threadsafe_queue<function_wrapper> pool_work_queue;
//局部任务队列组
std::vector<std::unique_ptr<work_stealing_queue>> queues;
std::vector<std::thread> threads;
join_threads joiner;
static thread_local work_stealing_queue* local_work_queue;
static thread_local unsigned m_index;
void worker_thread(unsigned index_)//初始化局部队列
{
m_index = index_;
local_work_queue = queues[m_index].get();
while (!done)
{
run_pending_task();
}
}
bool pop_from_local_queue(task_type& task)
{
return local_work_queue && local_work_queue->try_pop(task);
}
bool pop_from_pool_queue(task_type& task)
{
return pool_work_queue.try_pop(task);
}
//从任务队列组向其它任务队列窃取任务
bool pop_from_other_queue(task_type& task)
{
for (unsigned i = 1; i < queues.size(); ++i)
{
const unsigned index = (index + i) % queues.size();
if (queues[index]->try_steal(task))
{
return true;
}
}
return false;
}
public:
thread_pool() :done(false), joiner(threads)
{
const unsigned thread_count = std::thread::hardware_concurrency();
try
{
//先初始化局部任务队列组,因为初始化任务队列需要用到队列组的索引
for (unsigned i = 0; i < thread_count; ++i)
{
queues.push_back(std::unique_ptr<work_stealing_queue>(new work_stealing_queue()));
}
for (unsigned i = 0; i < thread_count; ++i)
{
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_from_local_queue(task) ||
pop_from_pool_queue(task) ||
pop_from_other_queue(task))
{
task();
}
else
{
std::this_thread::yield();
}
}
};
增设由线程池维护的局部任务队列组,用于存储局部任务队列,各线程凭索引值获得自身的局部任务队列,线程空闲时,就会尝试从队列组中获取其它线程专属的任务队列。
在窃取队列函数中,线程会逐个访问线程池内全部局部队列,其顺序从自身索引的后一个索引开始,避免了第一个索引对应的队列成为“众矢之的”。
2.中断线程
在许多情况下,如果线程运行时间过长,我们便想发送信号令其终止,其原因可能是线程池需要销毁,或用户取消执行的任务。我们需要一种通用的中断方式,以方便应对各种情形。(C++20中正式引入std::jthread,其下管控的线程可以接受中断,还能自动汇合)。
2.1可中断的线程
中断线程需要一个interrupt()函数,可以将thread_local变量作为中断专门定制的数据结构,启动线程时,将其准备妥当,当线程调用中断函数时,就查验该数据结构,得知此处是否可中断:
class interrupt_flag
{
public:
void set();
bool is_set() const;
};
thread_local interrupt_flag this_thread_interrupt_flag;
class interruptible_thread
{
std::thread internal_thread;
interrupt_flag* flag;
public:
template<typename FunctionType>
interruptible_thread(FunctionType f)
{
std::promise<interrupt_flag*> p;
internal_thread = std::thread([f, &p] {
p.set_value(&this_thread_interrupt_flag);
f(); })
flag = p.get_future().get();
}
void interrupt()
{
if (flag)//检验中断标志的指针
{
flag->set();//设置标志成立
}
}
};
调用者给出函数f()用于创建中断线程,构造函数发起新线程,将promise关联值设置为标志地址,再运行函数f(),此时发起调用的线程等待,直到将promise关联的值存储到flag中。注意线程分离或退出时,必须清除flag,避免指针悬空。
2.2检测线程是否被中断
我们已经可以设置中断标志位成立,但如果没有进行检测,终究是徒劳。简单的做法是,在可中断的线程内设置interruption_point()函数,中断后抛出异常:
void interruption_point()
{
if (this_thread_interrupt_flag.is_set())
{
throw thread_interrupted();
}
}
实际上,中断线程的最佳时机是线程因等待而发生阻塞时,但是若线程被阻塞,遂无法执行interruption_point(),故需要一种能被中断的等待方式。
2.3中断条件变量上的等待
我们需要引入一种新的函数——interruptible_wait(),可以重载以分别处理不同需要的事项。条件变量是一种需要等的事项,我们从条件变量入手,再interrupt_flag内部增添一个指针,指向相应的条件变量,set()的调用会通知它,以下是interruptible_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();//设置flag为可中断时需要通知所有线程
}
}
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 interrupt_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();
}
或需要断言的版本:
template<typename Predicate>
void interrupt_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();
}
2.4中断std::condition_variable_any上的等待
std::condition_variable_any与std::condition_variable的区别在于,前者可以配合任意型别的锁,后者仅限于std::unique_lock<std::mutex>。因此我们可以自定义构建锁,用于锁定/解锁interrupt_flag中的互斥:
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)
{}
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();//设置flag为可中断时需要通知所有线程
}
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,
Lockable& lk_) :self(self_), lk(lk_)
{
self->set_clear_mutex.lock();
self->thread_cond_any = &cond;
}
void unlock()
{
lk.unlock();
self->set_clear_mutex.unlock();
}
void lock()
{
std::lock(self->set_clear_mutex, lk);//给互斥和锁对象一起加锁
}
~custom_lock()
{
self->thread_cond_any = 0;
self->set_clear_mutex.unlock();
}
};
custom_lock cl(this, cv, lk);
interruption_point();
cv.wait(cl);//调用lock()加锁
interruption_point();
}
//以下代码与2.3节一样
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();
}
};
};
//外部调用时
template<typename Lockable>
void interruptible_wait(std::condition_variable_any& cv, Lockable& lk)
{
this_thread_interrupt_flag.wait(cv, lk);
}
在wait中,构造函数构建自定义锁的结构体,先在内部互斥set_clear_mutex上锁,获取内部锁(因为是引用,必须先完成锁定再保存到结构体内部)与条件变量。
wait()完成等待后,调用结构体自身的lock加锁。
接着重新检查中断,析构函数清除thread_cond_any指针,并解锁互斥。
2.5中断其他类型的阻塞
我们需要借助处理std::condition_variable用到的限时功能,因为上述等待行为不涉及等待某个条件成立,可以在interruptible_wait()函数中用循环等待(std::future<>的成员函数):
template<typename T>
void interruptible_wait(std::future<T>& uf)
{
while (!this_thread_interrupt_flag.is_set())
{
if (uf.wait_for(lk, std::chrono::microseconds(1)) == std::future_status::ready)
break;
}
interruption_point();
}
函数一直循环等待,知道中断标志位成立或future准备就绪。wait_for至少等待一个计时单元,缩短时间会使线程更频繁地唤醒。
2.6处理中断
以线程视角观察,中断本质上是一次thread_interrupt异常,可以用标准的try/catch块捕获:
try
{
do_something();
}
catch(thread_interrupted&)
{
handle_interruption();
}
令目标线程关联interruptible_thread对象,别的线程在该对象上调用interrupt()引发中断,程序捕获这个中断,放弃被中断的任务,线程继续执行下一项任务。
由于thread_interrrupted是异常,需要保证异常安全。我们希望中断结束线程,中断异常向上传播,但线程构造设定了函数,异常传播到函数外会被std::terminate()终止整个程序。为了防止该情形,我们在interruptible_thread构造函数中放置catch块:
template<typename FunctionType>
interruptible_thread(FunctionType f)
{
std::promise<interrupt_flag*> p;
internal_thread = std::thread([f, &p] {
p.set_value(&this_thread_interrupt_flag);
try
{
f();
}
catch (thread_interrupted const&)
{}
});
flag = p.get_future().get();
}
2.7在应用程序退出时中断后台任务
为了避免影响图形用户界面的交互响应能力,往往需要分离出线程处理后台任务,其贯穿应用程序运行周期的全过程,当应用程序关闭时,需要依次结束后台线程,其中一个做法是中断:
std::mutex config_mutex;
std::vector<interruptible_thread> background_threads;
void background_thread(int disk_id)
{
while (true)//后台线程不断更新索引
{
interruption_point();//判别是否中断
fs_change fsc = get_fs_changes(disk_id);
if (fsc.has_change())
{
update_index(fsc);
}
}
}
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();
process_gui_until_exit();//主线程负责图形界面
std::unique_lock<std::mutex> lk(config_mutex);
for (unsigned i = 0; i < background_threads.size(); ++i)
{
background_threads[i].interrupt();//退出时后台线程中断
}
for (unsigned i = 0; i < background_threads.size(); ++i)
{
background_threads[i].join();//后台线程汇合后主线程才退出
}
}
此处先中断全部线程再汇合全部线程,而不是中断一个汇合一个,是为了提高并发性能。线程不会因为中断就立马结束,它们必须先运行到中断点,调用析构函数,或处理异常,这样做令全体线程并行处理各自中断,只有每个线程都发起了中断,才让主线程等待汇合。
小结
线程池的基本构成:完成标志、线程组、线程安全的任务队列。
线程池的基本实现:线程池构建时,按照硬件参数创建线程并放入线程组,每个线程运行函数worker_thread()循环执行任务队列中的任务,直到完成标志成立(表示线程池被析构或抛出异常)。用户使用时,通过调用线程池的submit()接口往任务队列中压入任务,任务交由线程池处理。
支持等待的线程池:通过std::packaged_task包装任务,通过future等待返回结果。
支持相互等待的线程池:等待的线程通过调用run_pending_task()(实际是worker_thread()的外部接口,可在worker_thread()中调用)协助处理任务。
避免争夺:通过设置线程池内线程独有的局部任务队列(在worker_thread()内初始化),优先处理局部队列来实现。全局队列则存放局部队列指针为空的任务(没有经过线程池构造函数初始化指针)。
任务窃取的线程池:线程池内,除了全局队列和局部队列,增设局部任务队列组(通过索引跟踪所有局部队列),当局部线程和全局线程均空时,尝试从局部队列组中取出局部任务队列,执行局部任务队列中的任务。注意尝试窃取的顺序应错开(根据自身索引设置初始索引),避免所有线程都从第一个队列开始。
中断条件变量的等待:检查中断点、条件变量等待(wait_for)、检查中断点,std::condition_variable_any条件变量适用自定义锁。
中断其他阻塞:循环等待中断标志,并限时等待future准备就绪。
处理中断:在可中断线程类的构造函数中,引入try/catch块捕获内部线程的中断异常。
应用程序先中断所有后台线程,再进行汇合。