TimerThread
这次我们看看timer thread
之前已经大概知道他是干什么的了。在注释中也比较清楚
即在指定时间运行指定任务。注意运行任务的是独立的一个线程,添加任务和删除任务还是由调用者进行,而不是通过一些事件去分发。
先看看成员变量
这个stop原子量很常见了,用来停掉无限循环的代码的
option指的是timer thread的一些配置
timer thread是通过哈希来定位具体的任务的。所以我们有若干个bucket
mutex则用来保护nearest run time
_thread则是用来指定我们的工作线程的
核心函数也可以看到,就是schedule和unschedule。表示添加任务,以及删除任务
bucket中和哈希表的是一样的,一个mutex保护这个bucket,还有一个链表是task,以及一个nearest run time,等下看他的作用
可以发现侵入式的链表还是比较常见的。这里保存了下一个结点。具体的运行时间。运行的函数以及参数。这个任务对应的task id,以及version,用来判断这个任务是否还存活。
这里的task id和bthread的是一样的。同样是32位slot加上32位version
看schedule
先取一个task,然后跳过0号版本和1号版本
本质上是跳过0,因为0号代表他的版本,1号代表这个版本正在运行
然后锁住mutex,更新nearest run time,并将任务插入到链表中
然后看一下TimerThread的schedule
我们会根据他的pthread id来进行哈希。因为调用schedule是在pthread worker中进行的。通过pthread id进行哈希的话,同一个worker就会访问相同的bucket,从而获得更好的缓存局部性
如果earlier为true,说明这个任务更新了这个bucket中的最小值,那么他就有可能更新全局最小值。我们锁住全局的mutex并更新他。
并且如果他成功更新了全局最小,我们就通过futex去唤醒一个worker
unschedule。可以看到核心的逻辑就是用过CAS来让版本加2。如果失败的话说明这个任务可能正在运行了,或者任务早已经被其他人复用了。这时候我们通过比较expected version和id的版本加上一来判断具体的情况。
这里可以发现我们并没有在unschedule中去回收资源。而是在TimerThread::run中进行。
注释中有提到,在大多数的情况下,我们不在unschedule中去复用task是不会有影响的。否则的话我们就需要在unschedule和run中都处理回收的情况,从而导致增加代码复杂度。
感性理解的话,上面的公式大概就是在timerthread唤醒之前,我们可能会有很多unscheduled task。但是我们这里会为每个线程缓存128k的空间。也就是当这些未服用的task不超过这个缓存空间的时候,不会有额外的影响。当超过的时候,可能就会导致局部性的降低(因为这些任务不再能fit in cache)
timer thread的worker运行的逻辑在run中
清空最近的任务时间。否则我们一直取最小,他就会被锁定到某个值上,因为时间是向前推进的
consume task会把这个bucket拿出来,然后更新nearest run time
try delete会判断版本号,如果是已经删除的版本我们就回收他的资源。否则的话就加入到task列表中
push heap会根据任务的时间来把这个任务插入到堆中。堆顶就是有最小的时间的任务。
然后在这个线程中他会不断的尝试运行最近的任务。并且在运行的时候会去检查一下当前的全局最小值。如果当前堆中的任务时间大于这个全家最小值,说明有新的更近的任务被插入了。我们需要重新拉取一次任务
如果发现没有新的任务的话,或者最近的任务时间还没到,我们就休眠一会儿,防止忙等待。
我们会通过nsignal来防止在我们等待的时候有更近的任务出现。他会获取当前的signal数量。然后通过futex在nsignal上面等待。
如果有新的最小值出现,他会更新signal,我们会被唤醒。
如果没有的话我们会在futex上睡ptimeout这么久,这时候我们被唤醒之后,堆中最小的任务的时间也就到了。我们就可以顺利的执行他了。
这个判断很关键。如果有更小的任务,我们就不在futex上的等待,直接开始pull task。如果没有更小的任务,我们会把当前的最小时间赋给_nearest_run_time。因为这时候可能nearest run time还是一个比较大的值。如果有一个更小的任务更新了他,但是这个任务没有next run time小,那么我们就不应该被唤醒。
所以我们会重新赋值nearest run time,保证他的值是我们已知的最小值,这样才能保证被唤醒的时候是真的出现了更小的任务。而非虚假唤醒。
相当棒的实现,代码可读性也很高。
文章评论