![b7d33a8d626f037f82fa6a1597a1fdbe.png](https://i-blog.csdnimg.cn/blog_migrate/8525be8db3b9695da9db9a2d290972f2.jpeg)
Job System 2.0: Lock-Free Work Stealing – Part 5: Dependencies
本文翻译自1部分模块添加自己的理解,有兴趣的同学建议阅读原文。
这篇文章job system的最后一块内容:添加job之间的依赖。
系列其他文章
- Part 1 : 描述job system基本概念和工作偷取概念。
- Part 2 : 讲述线程独有的内存分配细节。
- Part 3 : 讨论工作偷取队列的无锁实现。
- Part 4 : 介绍高阶算法。
保持简单
整个系统保持简单,无所队列的实现相对较为复杂,但模块本身是自洽的,外部无感知,现在讨论其他模块保持简单。
过去的方式下,引入job system依赖需要引入第二个队列(储存目前还无法执行的job),我们前面提到的部分代码需要重构。
这次使用最简单的方法:用户通过连续项
指定依赖关系。之前的策略是"当job完成时,尝试执行其他可能在执行的job",替换为"一旦job完成,立刻执行其他所有的连续性job"。
在这种立刻执行jobs代表push这些job到job queue中,所以均衡负载可以接管并确保所有内核/线程都可以平等的竞争工作。代码上如下:
AddContinuation(ancestor, dependency1);
AddContinuation(ancestor, dependency2);
最简单的实现方案:全部的连续项关系作为data保存在Job结构中。
支持连续性
扩充Job结构到128bytes(从64bytes)允许我们存储如下数据:
struct Job
{
JobFunction function;
Job* parent;
int32_t unfinishedJobs;
char data[52];
int32_t continuationCount; // new
Job* continuations[15]; // new
};
实际代码中,我们可以节省一些空间,我们可以储存offset,而不用实际储存job指针。offset甚至不需要16bit。
这是我们能够支持52bytes数据和每个job 16个连续性工作,也可以平衡数据和工作连续项的大小关系。甚至可以合并两个数组,通过运行时assrt动态地存储。
给已有job添加连续项如下:
void AddContinuation(Job* ancestor, Job* continuation)
{
const int32_t count = atomic::Increment(&ancestor->coutinuationCount);
ansetor->continuations[count - 1] = continuation;
}
当然我们需要保证ancestor job是没有被执行的,并且保证连续项数组中仍然有空间能够放入。增加连续项的计数必须是原子操作,用以保证与其他线程中尝试增加连续项的操作没有数据竞争。
在job system中应用连续项也很简单,我们只需在finish后添加对应处理:
void Finish(Job* job)
{
[...]
if (job->parent)
{
Finish(job->parent);
}
// run follow-up jobs
for (int32_t i = 0; i < job->continuationCount; ++i) {
PushToQueue(job->continuations[i]);
}
}
这样的实现已经足够,这一版优点如下:
- 实现简单
- 完美处理job父子关系
- 不需要过多的堆栈空间,所有的空间都提前申请
现在每个job需要2倍的空间,对于现在的手机内存来讲并不是一个大问题。另外对于使用128-byte大小cache line的架构来说,需要增加job空间占用,避免false sharing。
总结
本文是Job System系列的最后一篇文章,希望你能够喜欢。
参考
[1] https://blog.molecular-matters.com/2016/04/04/job-system-2-0-lock-free-work-stealing-part-5-dependencies/