作为一般规则,失去对线程的跟踪是非常糟糕的。退出时让代码在另一个线程中运行main
是未定义行为的原因。
因此std::future
,返回的具有特殊属性,当它被销毁时它将std::async
等待完成。std::async
这就是您所说的“挂起”。这不是挂起 - 它正在等待任务完成。
C++11 中的线程原语是原语;它们不是功能齐全的应用程序的完成类型。你可以用它们写线程池延迟任务队列等;天真地“原始地”使用它们往往会导致它们偏向于正确性,但不会给你想要的东西。
一个简单的线程池就是:
template<class T>
struct threaded_queue {
using lock = std::unique_lock<std::mutex>;
void push_back( T t ) {
{
lock l(m);
data.push_back(std::move(t));
}
cv.notify_one();
}
boost::optional<T> pop_front() {
lock l(m);
cv.wait(l, [this]{ return abort || !data.empty(); } );
if (abort) return {};
auto r = std::move(data.back());
data.pop_back();
return std::move(r);
}
void terminate() {
{
lock l(m);
abort = true;
data.clear();
}
cv.notify_all();
}
~threaded_queue()
{
terminate();
}
private:
std::mutex m;
std::deque<T> data;
std::condition_variable cv;
bool abort = false;
};
struct thread_pool {
thread_pool( std::size_t n = 1 ) { start_thread(n); }
thread_pool( thread_pool&& ) = delete;
thread_pool& operator=( thread_pool&& ) = delete;
~thread_pool() = default; // or `{ terminate(); }` if you want to abandon some tasks
template<class F, class R=std::result_of_t<F&()>>
std::future<R> queue_task( F task ) {
std::packaged_task<R()> p(std::move(task));
auto r = p.get_future();
tasks.push_back( std::move(p) );
return r;
}
template<class F, class R=std::result_of_t<F&()>>
std::future<R> run_task( F task ) {
if (threads_active() >= total_threads()) {
start_thread();
}
return queue_task( std::move(task) );
}
void terminate() {
tasks.terminate();
}
std::size_t threads_active() const {
return active;
}
std::size_t total_threads() const {
return threads.size();
}
void clear_threads() {
terminate();
threads.clear();
}
void start_thread( std::size_t n = 1 ) {
while(n-->0) {
threads.push_back(
std::async( std::launch::async,
[this]{
while(auto task = tasks.pop_front()) {
++active;
try{
(*task)();
} catch(...) {
--active;
throw;
}
--active;
}
}
)
);
}
}
private:
std::vector<std::future<void>> threads;
threaded_queue<std::packaged_task<void()>> tasks;
std::atomic<std::size_t> active;
};
活生生的例子。
现在你在某个地方创建了一些线程池,向它扔任务,你可以等待有问题的期货。池中有有限数量的线程。
run_task
将确保有一个线程来运行您排队的任何任务。 queue_task
如果可用,将仅使用现有线程。
返回std::future<void>
的不会阻塞任务完成;但是thread_pool
对象的析构函数可以。
请注意,它将中止所有排队的任务,并等待当前正在运行的任务完成,默认情况下在销毁时。
包裹 aunique_ptr<thread_pool>
的东西有助于轻松移动。必须禁用移动,因为活动线程保持指向-的指针this
。
thread_pool
具有讽刺意味的是,它不是线程安全的;这是因为我们不保护std::vector<std::future<void>> threads;
; 我的意思是,除了线程本身存储的线程安全之外。它被设计为仅由一个外部线程直接访问。
queue_task
并且terminate
是线程安全的,主要是偶然的。