This is (supposedly) a multi-threaded scheduler for one-time and/or repeating tasks. The tasks are simple std::function<void()>
objects. I built it to be a crucial part of a larger project I'm working on, but I developed it stand-alone, so no context is missing for a review.
I'm making heavy use of C++11 language and library features (especially thread support and chrono stuff).
Tasks are supposed to be scheduled by specifying a start time_point
, or a delay (converted to a time_point
by adding it to now()
.) An optional duration specifies repeat intervals for the task (if it's non-zero).
It should be possible to de-schedule tasks, preventing them from being started for execution from then on. (Already running tasks won't be stopped, to keep things a bit simpler, and also because I couldn't figure out a clean way to do it anyway.)
I've never done anything with multithreading of this scale/complexity, and in case my brain never recovers from repeatedly being torn into 5 or more threads, I'd like to get some review/feedback from others. Specifically, race conditions/deadlocks/other threading-unpleasantness I didn't spot, lifetime issues, or really anything problematic.
Some simple code at the very bottom demonstrates how it's meant to be used. It seemed to work when compiled with clang 3.3 and libc++.
#include <chrono>
#include <condition_variable>
#include <deque>
#include <list>
#include <mutex>
#include <thread>
#include <utility>
#include <vector>
namespace scheduling {
template <class Clock>
class Scheduler {
typedef Clock clock_type;
typedef typename clock_type::time_point time_point;
typedef typename clock_type::duration duration;
typedef std::function<void()> task_type;
private:
struct Task {
public:
Task (task_type&& task, const time_point& start, const duration& repeat) : task(std::move(task)), start(start), repeat(repeat) { }
task_type task;
time_point start;
duration repeat;
bool operator<(const Task& other) const {
return start < other.start;
}
};
public:
typedef typename std::list<Task>::iterator task_handle;
private:
std::mutex mutex;
std::condition_variable tasks_updated;
std::deque<task_handle> todo;
std::condition_variable modified;
bool running;
std::list<Task> tasks;
std::list<task_handle> handles;
std::vector<std::thread> threads;
public:
Scheduler() : threads(4) {
}
~Scheduler() {
halt();
}
task_handle schedule(task_type&& task, const time_point& start, const duration& repeat=duration::zero()) {
task_handle h;
{
std::lock_guard<std::mutex> lk(mutex);
h = tasks.emplace(tasks.end(), std::move(task), start, repeat);
handles.push_back(h);
}
tasks_updated.notify_all();
return h;
}
task_handle schedule(task_type&& task, const duration& delay=duration::zero(), const duration& repeat=duration::zero()) {
return schedule(std::move(task, clock_type::now()+delay, repeat));
}
void unschedule(const task_handle& handle) {
{
std::lock_guard<std::mutex> lk(mutex);
auto handle_it = std::find(handles.begin(), handles.end(), handle);
if (handle_it != handles.end()) {
tasks.erase(handle);
todo.remove(handle);
handles.erase(handle_it);
}
}
tasks_updated.notify_all();
}
void clear() {
{
std::lock_guard<std::mutex> lk(mutex);
tasks.clear();
handles.clear();
}
tasks_updated.notify_all();
}
void run() {
{
std::lock_guard<std::mutex> lk(mutex);
if (running) return;
running = true;
for (auto& t : threads) {
t = std::thread([this]{this->loop();});
}
}
while (true) {
std::unique_lock<std::mutex> lk(mutex);
if (!running) break;
auto task_it = min_element(tasks.begin(), tasks.end());
time_point next_task = task_it == tasks.end() ? clock_type::time_point::max() : task_it->start;
if (tasks_updated.wait_until(lk, next_task) == std::cv_status::timeout) {
if (task_it->repeat != clock_type::duration::zero()) {
task_it->start += task_it->repeat;
}
else {
handles.remove(task_it);
tasks.erase(task_it);
}
todo.push_back(task_it);
modified.notify_all();
}
}
for (auto& t : threads) {
t.join();
}
}
void halt() {
{
std::lock_guard<std::mutex> lk(mutex);
if (!running) return;
running = false;
}
tasks_updated.notify_all();
modified.notify_all();
}
private:
void loop() {
while (true) {
std::function<void()> f;
{
std::unique_lock<std::mutex> lk(mutex);
while (todo.empty() && running) {
modified.wait(lk);
}
if (!running) {
return;
}
f = todo.front()->task;
todo.pop_front();
}
f();
}
}
};
}
#include <iostream>
void outp(const std::string& outp) {
static std::mutex m;
std::lock_guard<std::mutex> lk(m);
std::cout << std::this_thread::get_id() << ": " << outp << std::endl;
}
int main(int argc, char* argv[]) {
scheduling::Scheduler<std::chrono::steady_clock> sched;
sched.schedule([&sched]{outp("Task 1");}, std::chrono::steady_clock::now());
sched.schedule([&sched]{outp("Task 2");}, std::chrono::steady_clock::now()+std::chrono::seconds(2), std::chrono::seconds(2));
sched.schedule([&sched]{outp("Task 3");}, std::chrono::steady_clock::now()+std::chrono::seconds(2), std::chrono::seconds(2));
sched.schedule([&sched]{outp("Task 4");}, std::chrono::steady_clock::now()+std::chrono::seconds(2), std::chrono::seconds(2));
sched.schedule([&sched]{outp("Task 5");}, std::chrono::steady_clock::now()+std::chrono::seconds(2), std::chrono::seconds(2));
sched.schedule([&sched]{outp("Task 6");}, std::chrono::steady_clock::now()+std::chrono::seconds(3));
sched.schedule([&sched]{outp("Task 7");}, std::chrono::steady_clock::now()+std::chrono::seconds(3));
sched.schedule([&sched]{outp("Task 8");}, std::chrono::steady_clock::now()+std::chrono::seconds(3));
sched.schedule([&sched]{outp("Task 9");}, std::chrono::steady_clock::now()+std::chrono::seconds(3));
sched.schedule([&sched]{outp("Task 10"); sched.halt(); }, std::chrono::steady_clock::now()+std::chrono::seconds(5));
sched.run();
}
std::function
is not free and its type erasure mechanism comes at a run-time cost. Perhaps it's worth considering using templates with perfect forwarding to accept anything callable, similarly to howstd::async
does it (see how it takesFunction
): en.cppreference.com/w/cpp/thread/async \$\endgroup\$boost::packaged_task
(e.g., "task constructor") for a more complete example: boost.org/doc/libs/release/doc/html/thread/… Each lambda function is of a distinct "closure type" ("a unique unnamed non-union non-aggregate type", typically implemented as a function object): en.cppreference.com/w/cpp/language/lambda \$\endgroup\$<string>
and<algorithm>
without including those headers. \$\endgroup\$