5

关于 skynet 调度器的一点想法(续)

 3 years ago
source link: https://blog.codingnow.com/2021/01/new_schedule.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

关于 skynet 调度器的一点想法(续)

这篇是继续上次的一些想法

我最近想重新做一个新的基于消息管道的 N:M 多任务调度器。主要用在我们的客户端上。算是解决在使用 skynet 多年来总结出来的一些问题。

首先,我想改变 skynet 服务直接将待发消息投递到对方消息队列的做法。发出消息会让服务挂起,等待投递。

然后,我设计了一个叫做调度器的模块,用于把带发消息投递到对应的接收管道中。调度器是发送消息的消费者,接收管道的唯一生产者;而每个服务是它自己的发送消息的唯一生产者,接收管道的唯一消费者。这样没有竞争,当消息队列是定长的时候,也无需使用锁(但引入了消息队列满的新状态)。

同时,也遵循 skynet 的约定,send 消息是不会引起服务重入的。所以,因为发送消息而挂起的服务,调度器会尽快处理,给出结果,延续服务的运行。中间不会插入针对这个服务的别的事务。消息投递结果可能是成功,也可能是对方不存在,或对方队列阻塞。如果发送发送阻塞,逻辑层可以决定是重试还是丢弃。

这个方案是实现的重点是这个调度器。我想改变 skynet 现在每个工作线程去抢任务的方案。由(唯一的)调度器去分派任务。这样可以避免全局队列的锁,还可以让服务尽量在同一物理线程上工作。

实现细节我想了不少时间,方案主要是受最近 100+ 小时的异星工厂游戏时间的启发。

我原本想给每个工作线程安排一个工作队列,由调度器根据任务原本所在的工作线程,以及工作负荷均分的原则将任务投递进去。这些工作队列理论上只有工作线程本身单一消费者,和调度器这个唯一生产者。也可以简单实现。

但这里无法解决极端情况:我们无法预知未执行的任务到底需要多长时间。一旦一个任务花了超长的时间,就会导致所属工作线程上的工作队列中的任务全部收到延迟影响。如果再让调度器去检测病态任务,并收回已经分配任务重新分配的话,前面的简单性就不复存在。

所以,我不给工作线程安排工作队列,而只提供一个待处理任务的 slot (也可以视为一个长度为 1 的队列)。用原子指令操作。

调度器每轮的职责只是把待分配任务填到所有空的 slot 中就够了。而工作线程取出自己所属的非空 slot 就将其置空并执行任务。

这里有一个技巧。我们不必用一个独立线程去运行调度器。而把调度器视为一个模块,任何工作线程都可以运行,但运行前,需要先用 cas 指令抢到运行调度的权力。同一时刻,只有一个工作线程可以运行调度器。

如果工作线程在当前任务执行完毕后,发现所属的 slot 中没有下一份任务。那么它就尝试申请调度器的运行权力,如果获得调度权限,优先给自己分配下一个任务,然后把待做任务分到其它的 slot 中。如果没有待做任务,还可以把已经分配到其它 slot 上的任务收回,分给自己。

这样,就保证了只要有一个工作线程未休息,那么待做任务就至少有人会去做。

当工作线程无事可做,又抢不到调度权限时,它可以休息,但会对工作线程总数做原子的计数。如果它发现自己是最后一个准备去休息的工作线程了,那么它就负责把所有工作线程唤醒。这是为了避免边界情况导致有任务没做完但所有工作线程都休眠了。

只有在运行调度器的工作线程发现完全没有任何任务可以分配时,这个时候才真的全体休息,直到有外部消息(网络消息或时钟消息)进来,重新唤醒工作线程。

云风 提交于 January 22, 2021 11:38 AM | 固定链接


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK