如何在C ++中创建高效的多线程任务调度程序?

时间:2017-09-07 11:14:32

标签: c++ linux multithreading pthreads task

我想在C ++中创建一个非常有效的任务调度程序系统。

基本理念是:

class Task {
    public:
        virtual void run() = 0;
};

class Scheduler {
    public:
        void add(Task &task, double delayToRun);
};

Scheduler后面,应该有一个固定大小的线程池,它运行任务(我不想为每个任务创建一个线程)。 delayToRun表示task没有立即执行,但delayToRun秒之后(从添加到Scheduler的点开始计算)。

delayToRun当然意味着"至少"值。如果系统已加载,或者如果我们从调度程序中询问不可能的内容,则它无法通过处理我们的请求。但它应该尽力而为)

这就是我的问题。如何有效地实现delayToRun功能?我试图通过使用互斥锁和条件变量来解决这个问题。

我看到两种方式:

使用经理线程

计划程序包含两个队列:allTasksQueuetasksReadyToRunQueue。任务将allTasksQueue添加到Scheduler::add。有一个管理器线程,它等待的时间最短,因此它可以将任务从allTasksQueue发送到tasksReadyToRunQueue。工作线程等待tasksReadyToRunQueue中可用的任务。

如果Scheduler::addallTasksQueue前面添加了一项任务(任务,其值为delayToRun,那么它应该在当前最快运行任务之前),那么经理任务需要被唤醒,因此它可以更新等待的时间。

这种方法可以被认为是低效的,因为它需要两个队列,并且它需要两个condvar.signals才能运行任务(一个用于allTasksQueue - > tasksReadyToRunQueue,另一个用于发信号通知工作线程实际运行任务)

没有经理线程

调度程序中有一个队列。任务将在Scheduler::add添加到此队列中。工作线程检查队列。如果它是空的,它会在没有时间限制的情况下等待。如果它不为空,则等待最快的任务。

  1. 如果只有一个条件变量为工作线程等待:这个方法可以被认为是低效的,因为如果在队列前面添加了一个任务(前面意味着,如果有N个工作线程,那么任务索引< N)然后全部工作线程需要被唤醒以更新他们等待的时间。

  2. 如果每个线程都有一个单独的条件变量,那么我们可以控制唤醒哪个线程,所以在这种情况下我们不需要唤醒所有线程(我们只需要唤醒具有最大等待时间的线程,因此我们需要管理此值)。我目前正在考虑实现这一点,但确切的细节是很复杂的。是否有关于此方法的任何建议/想法/文件?

  3. 这个问题还有更好的解决方案吗?我试图使用标准的C ++功能,但我也愿意使用平台依赖(我的主要平台是linux)工具(如pthreads),甚至是linux特定的工具(如futexes),如果它们提供的话一个更好的解决方案。

5 个答案:

答案 0 :(得分:8)

你可以避免让一个单独的经理"线程,并且必须在下一个运行任务发生更改时唤醒大量任务,方法是使用一个设计,其中单个池线程等待"旁边运行"一个条件变量上的任务(如果有),其余池线程无限期地等待第二个条件变量。

池线程将沿着以下行执行伪代码:

pthread_mutex_lock(&queue_lock);

while (running)
{
    if (head task is ready to run)
    {
        dequeue head task;
        if (task_thread == 1)
            pthread_cond_signal(&task_cv);
        else
            pthread_cond_signal(&queue_cv);

        pthread_mutex_unlock(&queue_lock);
        run dequeued task;
        pthread_mutex_lock(&queue_lock);
    }
    else if (!queue_empty && task_thread == 0)
    {
        task_thread = 1;
        pthread_cond_timedwait(&task_cv, &queue_lock, time head task is ready to run);
        task_thread = 0;
    }
    else
    {
        pthread_cond_wait(&queue_cv, &queue_lock);
    }
}

pthread_mutex_unlock(&queue_lock);

如果更改下一个要运行的任务,则执行:

if (task_thread == 1)
    pthread_cond_signal(&task_cv);
else
    pthread_cond_signal(&queue_cv);

持有queue_lock

在这种方案下,所有唤醒都只直接在一个线程上,只有一个任务优先级队列,并且不需要经理线程。

答案 1 :(得分:6)

您的规格有点太强了:

  

delayToRun表示任务没有立即执行,但delayToRun秒后

你忘了添加"至少" :

  • 此任务现在无法执行,但至少 delayToRun秒后

重点是,如果一万个任务都安排了0.1 delayToRun,那么它们肯定无法同时运行。

通过这样的更正,您只需维护一些队列(或议程)(预定启动时间,运行闭包),保持该队列排序,然后启动N(某些固定数量)线程以原子方式弹出议程的第一个元素并运行它。

  

然后需要唤醒所有工作线程以更新他们等待的时间。

不,某些工作线程会被唤醒。

了解条件变量和广播。

您也可以使用POSIX计时器,请参阅timer_create(2)或Linux特定的fd计时器,请参阅timerfd_create(2)

您可能会避免在线程中运行阻塞系统调用,并使用某些中心线程来管理它们(请参阅poll(2) ...);否则,如果你有一百个任务正在运行sleep(100)而一个任务计划在半秒内运行,它将在一百秒之前运行。

您可能想要了解continuation-passing style编程(它与-CPS-高度相关)。阅读Juliusz Chroboczek撰写的paper about Continuation Passing C

同时查看Qt threads

你也可以考虑用Go(及其Goroutines)进行编码。

答案 2 :(得分:3)

这是您提供的界面的示例实现,它最接近您的使用经理线程'描述。

它使用单个线程(timer_thread)来管理队列(allTasksQueue),该队列根据必须启动任务的实际时间(std::chrono::time_point)进行排序。 /> '队列'是std::priority_queue(保持其time_point个关键元素排序)。

timer_thread通常会暂停,直到下一个任务开始或添加新任务为止 当一个任务即将被运行时,它被放置在tasksReadyToRunQueue中,其中一个工作线程被发出信号,被唤醒,将其从队列中删除并开始处理任务..

请注意,线程池具有线程数的编译时上限(40)。如果您安排的任务多于可以派遣给工人的任务, 新任务将阻塞,直到线程再次可用。

你说这种方法效率不高,但总的来说,它对我来说似乎相当有效。它是所有事件驱动的,你不会因不必要的旋转而浪费CPU周期。 当然,它只是一个示例,可以进行优化(注意:std::multimap已被std::priority_queue替换)。

该实现符合C ++ 11

#include <iostream>
#include <chrono>
#include <queue>
#include <unistd.h>
#include <vector>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <memory>

class Task {
public:
    virtual void run() = 0;
    virtual ~Task() { }
};

class Scheduler {
public:
    Scheduler();
    ~Scheduler();

    void add(Task &task, double delayToRun);

private:
    using timepoint = std::chrono::time_point<std::chrono::steady_clock>;

    struct key {
        timepoint tp;
        Task *taskp;
    };

    struct TScomp {
        bool operator()(const key &a, const key &b) const
        {
            return a.tp > b.tp;
        }
    };

    const int ThreadPoolSize = 40;

    std::vector<std::thread> ThreadPool;
    std::vector<Task *> tasksReadyToRunQueue;

    std::priority_queue<key, std::vector<key>, TScomp> allTasksQueue;

    std::thread TimerThr;
    std::mutex TimerMtx, WorkerMtx;
    std::condition_variable TimerCV, WorkerCV;

    bool WorkerIsRunning = true;
    bool TimerIsRunning = true;

    void worker_thread();
    void timer_thread();
};

Scheduler::Scheduler()
{
    for (int i = 0; i <ThreadPoolSize; ++i)
        ThreadPool.push_back(std::thread(&Scheduler::worker_thread, this));

    TimerThr = std::thread(&Scheduler::timer_thread, this);
}

Scheduler::~Scheduler()
{
    {
        std::lock_guard<std::mutex> lck{TimerMtx};
        TimerIsRunning = false;
        TimerCV.notify_one();
    }
    TimerThr.join();

    {
        std::lock_guard<std::mutex> lck{WorkerMtx};
        WorkerIsRunning = false;
        WorkerCV.notify_all();
    }
    for (auto &t : ThreadPool)
        t.join();
}

void Scheduler::add(Task &task, double delayToRun)
{
    auto now = std::chrono::steady_clock::now();
    long delay_ms = delayToRun * 1000;

    std::chrono::milliseconds duration (delay_ms);

    timepoint tp = now + duration;

    if (now >= tp)
    {
        /*
         * This is a short-cut
         * When time is due, the task is directly dispatched to the workers
         */
        std::lock_guard<std::mutex> lck{WorkerMtx};
        tasksReadyToRunQueue.push_back(&task);
        WorkerCV.notify_one();

    } else
    {
        std::lock_guard<std::mutex> lck{TimerMtx};

        allTasksQueue.push({tp, &task});

        TimerCV.notify_one();
    }
}

void Scheduler::worker_thread()
{
    for (;;)
    {
        std::unique_lock<std::mutex> lck{WorkerMtx};

        WorkerCV.wait(lck, [this] { return tasksReadyToRunQueue.size() != 0 ||
                                           !WorkerIsRunning; } );

        if (!WorkerIsRunning)
            break;

        Task *p = tasksReadyToRunQueue.back();
        tasksReadyToRunQueue.pop_back();

        lck.unlock();

        p->run();

        delete p; // delete Task
    }
}

void Scheduler::timer_thread()
{
    for (;;)
    {
        std::unique_lock<std::mutex> lck{TimerMtx};

        if (!TimerIsRunning)
            break;

        auto duration = std::chrono::nanoseconds(1000000000);

        if (allTasksQueue.size() != 0)
        {
            auto now = std::chrono::steady_clock::now();

            auto head = allTasksQueue.top();
            Task *p = head.taskp;

            duration = head.tp - now;
            if (now >= head.tp)
            {
                /*
                 * A Task is due, pass to worker threads
                 */
                std::unique_lock<std::mutex> ulck{WorkerMtx};
                tasksReadyToRunQueue.push_back(p);
                WorkerCV.notify_one();
                ulck.unlock();

                allTasksQueue.pop();
            }
        }

        TimerCV.wait_for(lck, duration);
    }
}
/*
 * End sample implementation
 */



class DemoTask : public Task {
    int n;
public:
    DemoTask(int n=0) : n{n} { }
    void run() override
    {
        std::cout << "Start task " << n << std::endl;;
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << " Stop task " << n << std::endl;;
    }
};

int main()
{
    Scheduler sched;

    Task *t0 = new DemoTask{0};
    Task *t1 = new DemoTask{1};
    Task *t2 = new DemoTask{2};
    Task *t3 = new DemoTask{3};
    Task *t4 = new DemoTask{4};
    Task *t5 = new DemoTask{5};

    sched.add(*t0, 7.313);
    sched.add(*t1, 2.213);
    sched.add(*t2, 0.713);
    sched.add(*t3, 1.243);
    sched.add(*t4, 0.913);
    sched.add(*t5, 3.313);

    std::this_thread::sleep_for(std::chrono::seconds(10));
}

答案 3 :(得分:1)

这意味着您希望使用某个订单连续运行所有任务。

您可以创建某种类型的按任务延迟堆栈(甚至链接列表)排序。当新任务到来时,您应该根据延迟时间将其插入位置(只需有效地计算该位置并有效地插入新任务)。

以任务堆栈(或列表)的头部开始运行所有任务。

答案 4 :(得分:1)

C ++ 11的核心代码:

#include <thread>
#include <queue>
#include <chrono>
#include <mutex>
#include <atomic>
using namespace std::chrono;
using namespace std;
class Task {
public:
    virtual void run() = 0;
};
template<typename T, typename = enable_if<std::is_base_of<Task, T>::value>>
class SchedulerItem {
public:
    T task;
    time_point<steady_clock> startTime;
    int delay;
    SchedulerItem(T t, time_point<steady_clock> s, int d) : task(t), startTime(s), delay(d){}
};
template<typename T, typename = enable_if<std::is_base_of<Task, T>::value>>
class Scheduler {
public:
    queue<SchedulerItem<T>> pool;
    mutex mtx;
    atomic<bool> running;
    Scheduler() : running(false){}
    void add(T task, double delayMsToRun) {
        lock_guard<mutex> lock(mtx);
        pool.push(SchedulerItem<T>(task, high_resolution_clock::now(), delayMsToRun));
        if (running == false) runNext();
    }
    void runNext(void) {
        running = true;
        auto th = [this]() {
            mtx.lock();
            auto item = pool.front();
            pool.pop();
            mtx.unlock();
            auto remaining = (item.startTime + milliseconds(item.delay)) - high_resolution_clock::now();
            if(remaining.count() > 0) this_thread::sleep_for(remaining);
            item.task.run();
            if(pool.size() > 0) 
                runNext();
            else
                running = false;
        };
        thread t(th);
        t.detach();
    }
};

测试代码:

class MyTask : Task {
public:
    virtual void run() override {
        printf("mytask \n");
    };
};
int main()
{
    Scheduler<MyTask> s;

    s.add(MyTask(), 0);
    s.add(MyTask(), 2000);
    s.add(MyTask(), 2500);
    s.add(MyTask(), 6000);
    std::this_thread::sleep_for(std::chrono::seconds(10));

}