9.1.4 任务队列

在生产者/消费者模型中,往往有一个任务队列,生产者将任务加入到任务队列,消费者从任务队列中取出任务进行处理。例如,在网络框架中,网络线程接收任务并加入到任务队列,工作线程不断地从任务队列取出任务进行处理。

最为常见的场景是系统有一个全局任务队列,所有网络线程和工作线程操作全局任务队列都需要首先获取独占锁,这种方式的锁冲突严重,将导致大量操作系统上下文切换(context switch)。为了解决这个问题,可以给每个工作线程分配一个任务队列,网络线程按照一定的策略选择一个任务队列并加入任务,例如随机选择或者选择已有任务个数最少的任务队列。

将任务加入到任务队列(随机选择):

1)将total_task_num原子加1(total_task_num为全局任务计数值);

2)通过total_task_num%工作线程数,计算出任务所属的工作线程;

3)将任务加入到该工作线程对应的任务队列中;

4)唤醒工作线程。

然而,如果某个任务的处理时间很长,就有可能出现任务不均衡的情况,即某个线程的任务队列中还有很多任务未被处理,其他线程却处于空闲状态。OceanBase采取了一种很简单的策略应对这种情况:每个工作线程首先尝试从对应的任务队列中获取任务,如果获取失败(对应的任务队列为空),那么,遍历所有工作线程的任务队列,直到获取任务成功或者遍历完成所有的任务队列为止。

除此之外,OceanBase还实现了LightyQueue用于解决全局任务队列锁冲突问题。LightyQueue的设计思想如下:

9.1.4 任务队列 - 图1

假设系统中有3个工作线程t1,t2和t3,全局任务队列中共有10个槽位。首先,t1,t2和t3分别等待1号,2号以及3号槽位。网络线程将任务加入1号槽位时唤醒t1,加入2号槽位时唤醒t2,加入3号槽位时唤醒t3。接着,t2很快将任务处理完成后等待4号槽位,t3等待5号槽位,t1等待6号槽位。网络线程将任务加入到4,5,6号槽位时将分别唤醒t2,t3和t1。通过这样的方式,每个工作线程在不同的槽位上等待,避免了全局锁冲突。

将任务加入到工作队列(push)的操作如下:

1)占据下一个push槽位;

2)将任务加入到该push槽位;

3)唤醒该push槽位上正在等待的工作线程。

工作线程从任务队列中获取任务(pop)的操作如下:

1)占据下一个pop槽位;

2)如果该pop槽位上有任务,则直接返回;

3)否则,工作线程在该pop槽位上等待直到被push操作唤醒或者超时。