我想在C ++中创建一个非常有效的任务调度程序系统。
基本理念是:
class Task {
public:
virtual void run() = 0;
};
class Scheduler {
public:
void add(Task &task, double delayToRun);
};
在Scheduler
后面,应该有一个固定大小的线程池,它运行任务(我不想为每个任务创建一个线程)。 delayToRun
表示task
没有立即执行,但delayToRun
秒之后(从添加到Scheduler
的点开始计算)。
(delayToRun
当然意味着"至少"值。如果系统已加载,或者如果我们从调度程序中询问不可能的内容,则它无法通过处理我们的请求。但它应该尽力而为)
这就是我的问题。如何有效地实现delayToRun
功能?我试图通过使用互斥锁和条件变量来解决这个问题。
我看到两种方式:
计划程序包含两个队列:allTasksQueue
和tasksReadyToRunQueue
。任务将allTasksQueue
添加到Scheduler::add
。有一个管理器线程,它等待的时间最短,因此它可以将任务从allTasksQueue
发送到tasksReadyToRunQueue
。工作线程等待tasksReadyToRunQueue
中可用的任务。
如果Scheduler::add
在allTasksQueue
前面添加了一项任务(任务,其值为delayToRun
,那么它应该在当前最快运行任务之前),那么经理任务需要被唤醒,因此它可以更新等待的时间。
这种方法可以被认为是低效的,因为它需要两个队列,并且它需要两个condvar.signals才能运行任务(一个用于allTasksQueue
- > tasksReadyToRunQueue
,另一个用于发信号通知工作线程实际运行任务)
调度程序中有一个队列。任务将在Scheduler::add
添加到此队列中。工作线程检查队列。如果它是空的,它会在没有时间限制的情况下等待。如果它不为空,则等待最快的任务。
如果只有一个条件变量为工作线程等待:这个方法可以被认为是低效的,因为如果在队列前面添加了一个任务(前面意味着,如果有N个工作线程,那么任务索引< N)然后全部工作线程需要被唤醒以更新他们等待的时间。
如果每个线程都有一个单独的条件变量,那么我们可以控制唤醒哪个线程,所以在这种情况下我们不需要唤醒所有线程(我们只需要唤醒具有最大等待时间的线程,因此我们需要管理此值)。我目前正在考虑实现这一点,但确切的细节是很复杂的。是否有关于此方法的任何建议/想法/文件?
这个问题还有更好的解决方案吗?我试图使用标准的C ++功能,但我也愿意使用平台依赖(我的主要平台是linux)工具(如pthreads),甚至是linux特定的工具(如futexes),如果它们提供的话一个更好的解决方案。
答案 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));
}