
쓰레드 풀은 쓰레드의 작업 대기소 개념이다. 여러 Thread가 대기를 하고 있고 작업을 넣어주면 알아서 스레드중 하나가 받아서 처리하고 다시 대기상태로 돌아오는 것이다.
queue를 이용하는 것이고 producer-consumer 이랑 기본 아이디어는 동일하다고 볼 수 있다.// 총 Worker 쓰레드의 개수.
size_t num_threads_;
// Worker 쓰레드를 보관하는 벡터.
std::vector<std::thread> worker_threads_;
size_t: 메모리, 객체등의 크기를 표현하기 위한 정수 타입. sizeof의 결과 타입이다. 크기가 가질 수 있는 최대 객체 크기만큼 표현이 가능하다.// 할일들을 보관하는 job 큐.
std::queue<std::function<void()>> jobs_;
std::condition_variable cv_job_q_;
std::mutex m_job_q_;
queue를 이용했는데, 큐는 멀티 스레드 환경에서 안전하지 않기 때문에 이를 보호할 mutex와 cv를 선언한다.// 모든 쓰레드 종료
bool stop_all;
ThreadPool::ThreadPool(size_t num_threads)
: num_threads_(num_threads), stop_all(false) {
worker_threads_.reserve(num_threads_);
for (size_t i = 0; i < num_threads_; ++i) {
worker_threads_.emplace_back([this]() { this->WorkerThread(); });
}
}
WorkerThread()를 실행하게 한다.void ThreadPool::WorkerThread() {
while (true) {
std::unique_lock<std::mutex> lock(m_job_q_);
cv_job_q_.wait(lock, [this]() { return !this->jobs_.empty() || stop_all; });
if (stop_all && this->jobs_.empty()) {
return;
}
// 맨 앞의 job 을 뺀다.
std::function<void()> job = std::move(jobs_.front());
jobs_.pop();
lock.unlock();
// 해당 job 을 수행한다 :)
job();
}
}
WorkerThread()에서는 무슨 일을 수행해야 할까?jobs_가 비워져 있지 않거나 stop_all이 true가 될때까지 대기한다.stop_all이 true고 작업이 없으면 종료한다. (작업 중지)jobs_의 맨 앞 작업을 꺼낸다.void ThreadPool::EnqueueJob(std::function<void()> job) {
if (stop_all) {
throw std::runtime_error("ThreadPool 사용 중지됨");
}
{
std::lock_guard<std::mutex> lock(m_job_q_);
jobs_.push(std::move(job));
}
cv_job_q_.notify_one();
}
jobs_에 밀어넣으면 된다.lock_guard<std::mutex>를 이용해 중간 블록에서만 락을 운용한다.m_job_q_ 락을 거는 이유는 이미 작업 처리중일 수 있기 때문이다.notify_one()을 통해 wait중인 스레드중 하나를 깨워 동작시킨다.ThreadPool::~ThreadPool() {
stop_all = true;
cv_job_q_.notify_all();
for (auto& t : worker_threads_) {
t.join();
}
}
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
namespace ThreadPool {
class ThreadPool {
public:
ThreadPool(size_t num_threads);
~ThreadPool();
// job 을 추가한다.
void EnqueueJob(std::function<void()> job);
private:
// 총 Worker 쓰레드의 개수.
size_t num_threads_;
// Worker 쓰레드를 보관하는 벡터.
std::vector<std::thread> worker_threads_;
// 할일들을 보관하는 job 큐.
std::queue<std::function<void()>> jobs_;
// 위의 job 큐를 위한 cv 와 m.
std::condition_variable cv_job_q_;
std::mutex m_job_q_;
// 모든 쓰레드 종료
bool stop_all;
// Worker 쓰레드
void WorkerThread();
};
ThreadPool::ThreadPool(size_t num_threads)
: num_threads_(num_threads), stop_all(false) {
worker_threads_.reserve(num_threads_);
for (size_t i = 0; i < num_threads_; ++i) {
worker_threads_.emplace_back([this]() { this->WorkerThread(); });
}
}
void ThreadPool::WorkerThread() {
while (true) {
std::unique_lock<std::mutex> lock(m_job_q_);
cv_job_q_.wait(lock, [this]() { return !this->jobs_.empty() || stop_all; });
if (stop_all && this->jobs_.empty()) {
return;
}
// 맨 앞의 job 을 뺀다.
std::function<void()> job = std::move(jobs_.front());
jobs_.pop();
lock.unlock();
// 해당 job 을 수행한다 :)
job();
}
}
ThreadPool::~ThreadPool() {
stop_all = true;
cv_job_q_.notify_all();
for (auto& t : worker_threads_) {
t.join();
}
}
void ThreadPool::EnqueueJob(std::function<void()> job) {
if (stop_all) {
throw std::runtime_error("ThreadPool 사용 중지됨");
}
{
std::lock_guard<std::mutex> lock(m_job_q_);
jobs_.push(std::move(job));
}
cv_job_q_.notify_one();
}
} // namespace ThreadPool
void work(int t, int id) {
printf("%d start \n", id);
std::this_thread::sleep_for(std::chrono::seconds(t));
printf("%d end after %ds\n", id, t);
}
int main() {
ThreadPool::ThreadPool pool(3);
for (int i = 0; i < 10; i++) {
pool.EnqueueJob([i]() { work(i % 3 + 1, i); });
}
}
이전의 thread pool은 개념적으로 매우 좋으나 사실 받는 인자가 없고 무조건 void를 반환하는 다소 유연하지 않은 thread pool이다. 따라서 이번에는 템플릿을 활용해 제대로 된 thread pool을 만들어 보자.
// job 을 추가한다.
template <typename F, typename... Args>
std::future<typename std::result_of<F(Args...)>::type>
EnqueueJob(F f, Args... args);
typename... 은 가변 타입 파라메터 팩으로 타입이 여러개 올 수 있는 것을 의미한다. 즉 함수에 인자가 몇개가 올지 모르기 때문에 가변으로 해둔 것이다.typename std::result_of<F(Args...)>::type을 반환한다.jobs_.push([f, args...]() { f(args...); });
package_task를 이용한다.using return_type = typename std::result_of<F(Args...)>::type;
std::packaged_task<return_type()> job(std::bind(f, args...));
package_task<타입 이름()>은 나중에 operator()가 호출이 되면 타입 값을 계산해서 promise/future 공유 상태에 저장하는 래퍼가 된다.bind는 f와 args...를 묶어서 job()을 호출하면 f(args...)를 호출하게 해준다.std::future<return_type> job_result_future = job.get_future();
{
std::lock_guard<std::mutex> lock(m_job_q_);
jobs_.push([&job]() { job(); });
}
future<return_type>을 하나 꺼낸다.namespace ThreadPool {
class ThreadPool {
public:
ThreadPool(size_t num_threads);
~ThreadPool();
// job 을 추가한다.
template <class F, class... Args>
std::future<typename std::result_of<F(Args...)>::type> EnqueueJob(
F f, Args... args);
private:
// 총 Worker 쓰레드의 개수.
size_t num_threads_;
// Worker 쓰레드를 보관하는 벡터.
std::vector<std::thread> worker_threads_;
// 할일들을 보관하는 job 큐.
std::queue<std::function<void()>> jobs_;
// 위의 job 큐를 위한 cv 와 m.
std::condition_variable cv_job_q_;
std::mutex m_job_q_;
// 모든 쓰레드 종료
bool stop_all;
// Worker 쓰레드
void WorkerThread();
};
ThreadPool::ThreadPool(size_t num_threads)
: num_threads_(num_threads), stop_all(false) {
worker_threads_.reserve(num_threads_);
for (size_t i = 0; i < num_threads_; ++i) {
worker_threads_.emplace_back([this]() { this->WorkerThread(); });
}
}
void ThreadPool::WorkerThread() {
while (true) {
std::unique_lock<std::mutex> lock(m_job_q_);
cv_job_q_.wait(lock, [this]() { return !this->jobs_.empty() || stop_all; });
if (stop_all && this->jobs_.empty()) {
return;
}
// 맨 앞의 job 을 뺀다.
std::function<void()> job = std::move(jobs_.front());
jobs_.pop();
lock.unlock();
// 해당 job 을 수행한다 :)
job();
}
}
ThreadPool::~ThreadPool() {
stop_all = true;
cv_job_q_.notify_all();
for (auto& t : worker_threads_) {
t.join();
}
}
template <class F, class... Args>
std::future<typename std::result_of<F(Args...)>::type> ThreadPool::EnqueueJob(
F f, Args... args) {
if (stop_all) {
throw std::runtime_error("ThreadPool 사용 중지됨");
}
using return_type = typename std::result_of<F(Args...)>::type;
std::packaged_task<return_type()> job(std::bind(f, args...));
std::future<return_type> job_result_future = job.get_future();
{
std::lock_guard<std::mutex> lock(m_job_q_);
jobs_.push([&job]() { job(); });
}
cv_job_q_.notify_one();
return job_result_future;
}
} // namespace ThreadPool
int work(int t, int id) {
printf("%d start \n", id);
std::this_thread::sleep_for(std::chrono::seconds(t));
printf("%d end after %ds\n", id, t);
return t + id;
}
int main() {
ThreadPool::ThreadPool pool(3);
std::vector<std::future<int>> futures;
for (int i = 0; i < 10; i++) {
futures.emplace_back(pool.EnqueueJob(work, i % 3 + 1, i));
}
for (auto& f : futures) {
printf("result : %d \n", f.get());
}
}
0 start
5 start
5 start
libc++abi: terminating due to uncaught exception of type
std::__1::future_error: The associated promise has been
destructed prior to the associated state becoming ready.
std::future<typename std::result_of<F(Args...)>::type> ThreadPool::EnqueueJob(
F f, Args... args) {
if (stop_all) {
throw std::runtime_error("ThreadPool 사용 중지됨");
}
using return_type = typename std::result_of<F(Args...)>::type;
std::packaged_task<return_type()> job(std::bind(f, args...));
std::future<return_type> job_result_future = job.get_future();
{
std::lock_guard<std::mutex> lock(m_job_q_);
jobs_.push([&job]() { job(); });
}
cv_job_q_.notify_one();
return job_result_future;
}
[&job](){job();}이 접근하려고 할때 이미 그 객체는 파괴된 상태가 되는 것이다.auto job =
std::make_shared<std::packaged_task<return_type()>>(std::bind(f, args...));
std::future<return_type> job_result_future = job->get_future();
{
std::lock_guard<std::mutex> lock(m_job_q_);
jobs_.push([job]() { (*job)(); });
}
auto job =
std::make_shared<std::packaged_task<return_type()>>(std::bind(f, args...));
ThreadPool::EnqueueJob(F f, Args... args);
std::forward()를 통해 해결할 수 있다.template <class F, class... Args>
std::future<typename std::result_of<F(Args...)>::type> EnqueueJob(
F&& f, Args&&... args);
&&를 템플릿에서 사용하면 "복사하지 말고, 넘어온 그대로(IValue or RValue)받아서 다음으로 넘길 준비를 한다는 뜻이다.auto job = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));