概要
此執行緒池擁有一個被所有工作執行緒共享的任務佇列。執行緒池使用者提交的任務,被執行緒池儲存在任務佇列中,工作執行緒從任務佇列中獲取任務並執行。
任務是可擁有返回值的、無引數的可呼叫(callable)物件,或者是經 std::bind 繫結了可呼叫物件及其引數後的呼叫包裝器。具體而言可以是
- 自由函式(也稱為全域性函式)
- lambda
- 函式物件(也稱為函式符)
- 類成員函式
- 包裝了上述型別的 std::function
- bind 呼叫包裝器
該執行緒池非同步地執行任務。當任務被提交進執行緒池後,使用者不必等待任務執行和返回結果。
實現
以下程式碼給出了此執行緒池的實現。
class Thread_Pool {
private:
struct Task_Wrapper { ...
};
atomic<bool> _done_; // #2
Lockwise_Queue<Task_Wrapper> _queue_; // #3
unsigned _workersize_;
thread* _workers_; // #4
void work() {
while (!_done_.load(memory_order_acquire)) {
Task_Wrapper task;
if (_queue_.pop(task))
task();
else
std::this_thread::yield();
}
}
public:
Thread_Pool() : _done_(false) { // #1
try {
_workersize_ = thread::hardware_concurrency(); // #5
_workers_ = new thread[_workersize_];
for (unsigned i = 0; i < _workersize_; ++i) {
_workers_[i] = thread(&Thread_Pool::work, this); // #6
}
} catch (...) { // #7
_done_.store(true, memory_order_release);
for (unsigned i = 0; i < _workersize_; ++i) {
if (_workers_[i].joinable())
_workers_[i].join();
}
delete[] _workers_;
throw;
}
}
~Thread_Pool() {
_done_.store(true, memory_order_release);
for (unsigned i = 0; i < _workersize_; ++i) {
if (_workers_[i].joinable())
_workers_[i].join();
}
delete[] _workers_;
}
template<class Callable>
future<typename std::result_of<Callable()>::type> submit(Callable c) { // #8
typedef typename std::result_of<Callable()>::type R;
packaged_task<R()> task(c);
future<R> r = task.get_future();
_queue_.push(std::move(task)); // #9
return r; // #10
}
};
我們從構造 Thread_Pool 物件(#1)開始瞭解這個執行緒池。atomic<bool> 資料成員用於標誌執行緒池是否結束,並強制同步記憶體順序(#2);Task_Wrapper 具體化了執行緒安全的任務佇列 Lockwise_Queue<>(#3);thread* 用於引用所有的工作執行緒物件(#4)。Task_Wrapper 和 Lockwise_Queue<> 稍後再做說明。
執行緒池通過 thread::hardware_concurrency() 獲取當前硬體支援的併發執行緒數量(#5),並依據此數量建立出工作執行緒。Thread_Pool 物件的成員函式 work() 作為所有工作執行緒的初始函式(#6),這使得執行緒池中的任務佇列能被所有工作執行緒共享。建立 thread 物件和 new 操作可能失敗並引發異常,因此用 try-catch 捕獲潛在的異常。處理異常過程中,需要標誌執行緒池結束,保證任何建立的執行緒都能正常的停止,並回收記憶體資源(#7)。執行緒池物件析構時的工作與此一致。
Thread_Pool 物件構建完成後,任務通過 Thread_Pool::submit<>() 被提交進入執行緒池(#8)。為了支援任務的非同步執行,任務先被封裝在 std::packaged_task<> 中,再被放入執行緒安全的任務佇列(#9)。任務執行結果被封裝在返回的 std::future<> 物件中(#10),允許使用者在未來需要結果時,等待任務結束並獲取結果。
因為每一個任務都是一個特定型別的 std::packaged_task<> 物件,為了實現任務佇列的泛型化,需要設計一個通用的資料結構 Task_Wrapper,用於封裝特定型別的 std::packaged_task<> 物件。
struct Task_Wrapper {
struct Task_Base {
virtual ~Task_Base() {}
virtual void call() = 0;
};
template<class T>
struct Task : Task_Base { // #5
T _t_;
Task(T&& t) : _t_(std::move(t)) {} // #6
void call() { _t_(); } // #9
};
Task_Base* _ptr_; // #7
Task_Wrapper() : _ptr_(nullptr) {};
template<class T>
Task_Wrapper(T&& t) : _ptr_(new Task<T>(std::move(t))) {} // #1
// support move
Task_Wrapper(Task_Wrapper&& other) { // #2
_ptr_ = other._ptr_;
other._ptr_ = nullptr;
}
Task_Wrapper& operator=(Task_Wrapper&& other) { // #3
_ptr_ = other._ptr_;
other._ptr_ = nullptr;
return *this;
}
// no copy
Task_Wrapper(Task_Wrapper&) = delete;
Task_Wrapper& operator=(Task_Wrapper&) = delete;
~Task_Wrapper() {
if (_ptr_) delete _ptr_;
}
void operator()() const { // #4
_ptr_->call(); // #8
}
};
std::packaged_task<> 的例項只是可移動的,而不可複製。Task_Wrapper 必須能移動封裝 std::packaged_task<R()> 物件(#1)。為了保持一致性,Task_Wrapper 也實現了移動構造(#2)和移動賦值(#3),同時實現了 operator()(#4)。ABC 的繼承結構(#5)用於支援泛型化地封裝和呼叫 std::packaged_task<> 物件。std::packaged_task<> 封裝在派生類 Task<> 中(#6),由指向非泛型的抽象基類 Task_Base 的指標引用派生類物件(#7)。對 Task_Wrapper 物件的呼叫由虛呼叫(#8)委託給派生類並執行實際的任務(#9)。
另一個關鍵的資料結構是執行緒安全的任務佇列 Lockwise_Queue<>。
template<class T>
class Lockwise_Queue {
private:
struct Spinlock_Mutex { // #3
atomic_flag _af_;
Spinlock_Mutex() : _af_(false) {}
void lock() {
while (_af_.test_and_set(memory_order_acquire));
}
void unlock() {
_af_.clear(memory_order_release);
}
} mutable _m_; // #2
condition_variable _cv_;
queue<T> _q_; // #1
public:
Lockwise_Queue() {}
void push(const T& element) {
lock_guard<Spinlock_Mutex> lk(_m_);
_q_.push(std::move(element));
_cv_.notify_one();
}
void push(T&& element) { // #4
lock_guard<Spinlock_Mutex> lk(_m_);
_q_.push(std::move(element));
_cv_.notify_one();
}
bool pop(T& element) { // #5
lock_guard<Spinlock_Mutex> lk(_m_);
if (_q_.empty())
return false;
element = std::move(_q_.front());
_q_.pop();
return true;
}
bool empty() const {
lock_guard<Spinlock_Mutex> lk(_m_);
return _q_.empty();
}
};
所有 Task_Wrapper 物件儲存在 std::queue<> 中(#1)。互斥元和條件變數控制工作執行緒對任務佇列的併發訪問(#2)。為了提高併發程度,採用非阻塞自旋鎖作為互斥元(#3)。任務的入隊和出隊操作,分別由支援移動語義的 push 函式(#4) 和 pop 函式(#5)完成。
驗證
為了驗證此執行緒池滿足概要中描述的能力,設計瞭如下的各類可呼叫物件。
void shoot() {
std::printf("\n\t[Free Function] Let an arrow fly...\n");
}
bool shoot(long n) {
std::printf("\n\t[Free Function] Let %ld arrows fly...\n", n);
return false;
}
auto shootAnarrow = [] {
std::printf("\n\t[Lambda] Let an arrow fly...\n");
};
auto shootNarrows = [](long n) -> bool {
std::printf("\n\t[Lambda] Let %ld arrows fly...\n", n);
return true;
};
class Archer {
public:
void operator()() {
std::printf("\n\t[Functor] Let an arrow fly...\n");
}
bool operator()(long n) {
std::printf("\n\t[Functor] Let %ld arrows fly...\n", n);
return false;
}
void shoot() {
std::printf("\n\t[Member Function] Let an arrow fly...\n");
}
bool shoot(long n) {
std::printf("\n\t[Member Function] Let %ld arrows fly...\n", n);
return true;
}
};
對這些函式做好必要的引數封裝,將其提交給執行緒池,
atomic<bool> go(false);
time_point<steady_clock> start = steady_clock::now();
minutes PERIOD(1);
Thread_Pool pool;
thread t1([&go, &pool, &PERIOD, start] { // test free function of void()
while (!go.load(memory_order_acquire))
std::this_thread::yield();
void (*task)() = shoot;
for (long x = 0; steady_clock::now() - start <= PERIOD; ++x) {
pool.submit(task);
//pool.submit(std::bind<void(*)()>(shoot));
std::this_thread::yield();
}
});
thread t2([&go, &pool, &PERIOD, start] { // test free function of bool(long)
while (!go.load(memory_order_acquire))
std::this_thread::yield();
bool (*task)(long) = shoot;
for (long x = 2; steady_clock::now() - start <= PERIOD; ++x) {
future<bool> r = pool.submit(std::bind(task, x));
//future<bool> r = pool.submit(std::bind<bool(*)(long)>(shoot, x));
std::this_thread::yield();
}
});
thread t3([&go, &pool, &PERIOD, start] { // test lambda of void()
while (!go.load(memory_order_acquire))
std::this_thread::yield();
for (long x = 0; steady_clock::now() - start <= PERIOD; ++x) {
pool.submit(shootAnarrow);
std::this_thread::yield();
}
});
thread t4([&go, &pool, &PERIOD, start] { // test lambda of bool(long)
while (!go.load(memory_order_acquire))
std::this_thread::yield();
for (long x = 2; steady_clock::now() - start <= PERIOD; ++x) {
future<bool> r = pool.submit(std::bind(shootNarrows, x));
std::this_thread::yield();
}
});
thread t5([&go, &pool, &PERIOD, start] { // test functor of void()
while (!go.load(memory_order_acquire))
std::this_thread::yield();
Archer hoyt;
for (long x = 0; steady_clock::now() - start <= PERIOD; ++x) {
pool.submit(hoyt);
std::this_thread::yield();
}
});
thread t6([&go, &pool, &PERIOD, start] { // test functor of bool(long)
while (!go.load(memory_order_acquire))
std::this_thread::yield();
Archer hoyt;
for (long x = 2; steady_clock::now() - start <= PERIOD; ++x) {
future<bool> r = pool.submit(std::bind(hoyt, x));
std::this_thread::yield();
}
});
thread t7([&go, &pool, &PERIOD, start] { // test member function of void()
while (!go.load(memory_order_acquire))
std::this_thread::yield();
Archer hoyt;
for (long x = 0; steady_clock::now() - start <= PERIOD; ++x) {
pool.submit(std::bind<void(Archer::*)()>(&Archer::shoot, &hoyt));
//pool.submit(std::bind(static_cast<void(Archer::*)()>(&Archer::shoot), &hoyt));
std::this_thread::yield();
}
});
thread t8([&go, &pool, &PERIOD, start] { // test member function of bool(long)
while (!go.load(memory_order_acquire))
std::this_thread::yield();
Archer hoyt;
for (long x = 2; steady_clock::now() - start <= PERIOD; ++x) {
future<bool> r = pool.submit(std::bind<bool(Archer::*)(long)>(&Archer::shoot, &hoyt, x));
//future<bool> r = pool.submit(std::bind(static_cast<bool(Archer::*)(long)>(&Archer::shoot), &hoyt, x));
std::this_thread::yield();
}
});
thread t9([&go, &pool, &PERIOD, start] { // test std::function<> of void()
while (!go.load(memory_order_acquire))
std::this_thread::yield();
std::function<void()> task = static_cast<void(*)()>(shoot);
for (long x = 0; steady_clock::now() - start <= PERIOD; ++x) {
pool.submit(task);
std::this_thread::yield();
}
});
thread t10([&go, &pool, &PERIOD, start] { // test std::function<> of bool(long)
while (!go.load(memory_order_acquire))
std::this_thread::yield();
std::function<bool(long)> task = static_cast<bool(*)(long)>(shoot);
for (long x = 2; steady_clock::now() - start <= PERIOD; ++x) {
future<bool> r = pool.submit(std::bind(task, x));
std::this_thread::yield();
}
});
編譯程式碼 g++ -std=c++11 a_simple_thread_pool.cpp
成功後執行 ./a.out
。以下是執行過程中的部分輸出,
...
[Functor] Let an arrow fly...
[Free Function] Let 9224 arrows fly...
[Free Function] Let 9445 arrows fly...
[Member Function] Let 9375 arrows fly...
[Lambda] Let 9449 arrows fly...
[Free Function] Let an arrow fly...
[Lambda] Let an arrow fly...
[Member Function] Let an arrow fly...
[Functor] Let 9469 arrows fly...
...
最後
完整示例請參考 [github] a_simple_thread_pool 。
作者參考了 C++併發程式設計實戰 / (美)威廉姆斯 (Williams, A.) 著; 周全等譯. - 北京: 人民郵電出版社, 2015.6 (2016.4重印) 一書中的部分設計思路。藉此機會對 Anthony Williams 及周全等譯者表示感謝。