1
\$\begingroup\$

Motivation: transform sync code

class ProgressController {
    bool isTerminationRequested() { ... }
    void terminate() { ... }
};


class MainWindow {
public:
    void onOpenImage(const std::string & filename) {
        auto image = readImage(filename, m_progressController); //lengthy task
        showImage(image);
    }
};

into async code with following requirements:

  1. Only one thread should be allocated for similar tasks.

  2. Tasks can have different implementation and grouped only logically (for example we can have onOpenVideo() action, and want it to cancel any running "readImage()" task)

  3. We are interested only in last called action. So if currently running similar task, we should cancel one if possible.

  4. Task lambda can be non-copyable (if captures values by move).

Possible call:

class MainWindow {
public:
    void onOpenImage(const std::string & filename) {
        std::shared_ptr<ProgressController> controller; // controller will live while both lambdas alive
        single_task::schedule(m_openFileTask, [controller, filename, this]() {
            auto image = readImage(filename, controller);
            showImage(image);
        }, [controller]() {
            controller.terminate();
        });
    }
    void onOpenVideo(const std::string & filename) {
        //...
    }
private:
    single_task m_openFileTask;
};

Implementation:

class single_task {
public:
    class CallableTask {
    public:
        virtual ~CallableTask() {}
        virtual void call() = 0;
        virtual void cancel() {}
    };

    single_task() : m_terminate(false) {
        m_thread = std::thread([this]() {
            do {
                {
                    std::unique_lock<std::mutex> lock(m_mutex);
                    m_task = std::move(m_nextTask);
                    while (!m_task && !m_terminate) {
                        m_taskChanged.wait(lock);
                        m_task = std::move(m_nextTask);
                    }
                }
                if (m_task) {
                    m_task->call();

                    m_mutex.lock();
                    m_task = nullptr;
                    m_mutex.unlock();
                }
            } while (!m_terminate);
        });
    }
    ~single_task() {
        cancel();
        m_terminate = true;
        m_taskChanged.notify_all();
        m_thread.join();
    }
    template <typename T>
    static void schedule(single_task & st, T && task) {
        auto t = new AutoTask<T>(std::move(task));
        st.setTask(std::unique_ptr<CallableTask>(t));
    }
    template <typename T, typename U>
    static void schedule(single_task & st, T && task, U && cancel_func) {
        auto t = new CancelableAutoTask<T, U>(std::move(task), std::move(cancel_func));
        st.setTask(std::unique_ptr<CallableTask>(t));
    }
    void cancel() {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (m_task)
            m_task->cancel();
        m_nextTask = nullptr;
    }
private:
    //m_task - currently running task
    std::unique_ptr<CallableTask> m_task, m_nextTask;
    std::mutex m_mutex;
    std::condition_variable m_taskChanged;
    std::thread m_thread;
    std::atomic<bool> m_terminate;

    template <typename T>
    class AutoTask : public CallableTask {
    public:
        AutoTask(T && f) : m_fn(std::move(f)) {}
        void call() override { m_fn(); }
    private:
        T m_fn;
    };

    template <typename T, typename U>
    class CancelableAutoTask : public AutoTask<T> {
    public:
        CancelableAutoTask(T && f, U && cancel)
            : AutoTask(std::move(f)), m_cancel(std::move(cancel))
        {}
        void cancel() override {
            m_cancel();
        }
    private:
        U m_cancel;
    };

    void setTask(std::unique_ptr<CallableTask> t) {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (m_task)
            m_task->cancel();
        m_nextTask = std::move(t);
        m_taskChanged.notify_all();
    }
};

Questions: Are there any existing patterns or implementations for this task? Can it be simplified (possibly using async or packaged tasks)?

Comments and suggestions are appreciated.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.