Why on the earth do we need thread-pool? The answer is obvious: for doing jobs behind the scenes.
That is, saying, you have a constant stream of incoming tasks to complete, and most of which either incur heavy computation or invovle device I/O, you definitely don’t want to execute them on your main thread, because it will block your main thread until the job is done, making your application less responsive.
However, with thread-pool, you can simply submit a task to the pool, then continue what was doing; the task will eventually be completed on a thread of the thread-pool.
If your processor has multiple cores, the task is possibly performed concurrently with your jobs on the main thread.
The number of running threads in the pool should be customizable to users, and this number usually depends on your available hardware cores and your current threading model.
So our TinyThreadPool should have a ctor that receives a size parameter.
explicit TinyThreadPool(size_t num);
Essentially, the running pattern of worker threads is a simple producer-consumer model:
- the pool maintains a task-queue storing incoming tasks
- users are producers submiting a task to the queue
- worker threads are consumers that extract a task out of the queue and execute it
and of course, operations on the task-queue are thread-safe.
Each worker thread runs a loop, and it
- blocks if the task-queue is empty
- wakes up (if had went to sleep) and extracts a task to execute
For the task-queue,
std::deque<> may not be the best, but neither will it be a bad choice; and for simplicity, we just ignore the case in which the task-queue is flooded by pending tasks, and simply leave it unbounded.
Synchronization operations on the task-queue can be done with
std::condition_variable, which happens to be the foundation of classic blocking-queue implementation.
Our submitting task interface,
PostTask(), accepts a callable object and its variadic arguments, and all of these are passed as forwarded via
Internally, we wrap the callable object in a
Task instance, defined as:
using Task = std::function<void()>;
and therefore, the task-queue is defined as:
To keep communication with submitted tasks, we use
std::future<> as the intermedia, which brings us two more problems:
- we need to figure out the return type of a task, which instantiates a
std::result_ofis made for this.
- we also need a way to create a future object; to keep implementation simple, we choose
template<typename F, typename... Args>
Note the reason we use
When a thread-pool tears down, it is possible there still are tasks pending in the queue.
Shall we pause to wait until all pending tasks get completed, or just abandon them.
One compromise is that we introduce concept of shutdown-behavior, which can either be
If a task chooses to block during shutdown, then the teardown blocks until all tasks in that behavior complete, and any skip-on-shutdown tasks will be skipped.
Sure, thread-pool’s dtor will wait for all worker threads to quit
Full code can be found at Eureka/TinyThreadPool