More than code

More Than Code
The efficiency of your iteration of reading, practicing and thinking decides your understanding of the world.
  1. 首页
  2. 未分类
  3. 正文

brpc-7 WorkStealingQueue

2022年6月7日 536点热度 1人点赞 0条评论

WorkStealingQueue

这次我们看看bthread比较关键的两个queue,work stealing queue,以及remote queue

先看work stealing queue

20220607141625

从这个成员变量就可以大概猜出来一些东西。这应该是一个循环队列类似的东西。最朴素的队列应该不太可能,因为性能太差。当然也有可能是一个batch一个batch的分配元素,如果是那样的话我们需要内部维护一个链表,但是这里看貌似什么都没有。

20220607142145

初始化部分可以看到我们是按照2的次幂的大小进行分配空间的

20220607142251

注释中可以看到,push可能与steal并发执行,但是不可能与pop和push并发执行。这是因为一个pthread worker只会在自己的queue上执行push和pop,而在其他人的queue上执行steal

他不会动态的扩大容量,而是在空间满的时候就返回false

以2的次幂为大小的好处就是mod的时候可以简单的通过位运算来实现

然后移动bottom,bottom为队尾,top为队头。这里我们不需要通过原子加来改变bottom,因为不会有并发的插入

20220607144629

这里的atomic thread fence就是指定这里的memory order,而没有关联的原子操作

表示的是在所有线程的视角中,一定是先有bottom = newb后,才会有加载top的指令。

这里的含义我感觉就是希望看到最新的top,并且要保证bottom已经被保存了。否则的话就有可能pop出一个错误的值。这里我们保证bottom已经减一,然后再加载top,再去比较,就能保证当t小于new b的时候,其他人不可能把我们当前拿到的值pop走,因为他们在看到top的时候,必定已经发现bottom被修改了。

这里的细节很多,由于我们不会有多个线程同时写bottom,所以bottom的写入可以是relaxed的。

这个atomic thread fence + seq cst memory order会生成full fence,所以我们的top是可以读到最新的值的。

这里我们优先从队尾进行pop,从而减少冲突。只有当发现t = newb,也就是当前队列只有一个元素的时候,我们才会通过compare exchange来进行竞争。

20220607165432

而这里的steal,是从远端的线程执行的,所以我们需要一些比较强的语义。通过acquire load确保读到最新的值。注意我们上面pop中最后存储bottom的时候是relaxed的语义,这并不保证被acquire读到,但是这不会影响结果。因为假如有后续的push,他会通过release来更新bottom。假如没有push,那么无论如何也没有新的元素,steal仍然会返回false

这里注释说了允许false negative,也就是说可能有新的值但是没有被读到。(但是我想不到false negative的情况,因为这要求b是stale value,然而可以进入这个分支的stale value说明这个stale value小于最新的bottom,这只有两种情况,一种是pop中的单元素情况,此时对这个元素的竞争已经结束了,第二种就是pop中t > newb的情况,这时候即便是新的b会进入这个分支)

然后他会尝试不断的通过CAS去拿一个元素,直到队列为空,或者成功获取这个元素为止。这里的load也是acquire的,为了读到最新的值。但是有一点比较奇怪,他会先拷贝这个元素,再去做CAS,这样当T比较大的时候开销会比较大。我个人感觉应该是成功CAS之后再去拿这个元素。

可以看到为了性能考虑这里的实现是相当精妙的。并且充足的考虑了程序的语义。分割开了steal和pop,让本地的pop从队尾进行,从而避免了seq cst的开销。让remote worker通过CAS去竞争。

remote task queue就是一个用mutex保护的queue而已,所以不用细说。

仔细想一下貌似pop中的fence不能保证bottom被其他的线程看到。这时候其他的线程会不断pop直到超过b,然后我们load top,发现top > newb,我们就恢复b,然后返回false。这里感觉fence的意义不大?如果把top的load换成seq cst load会不会效果是一样的呢?

我目前猜测可能是语义的原因,即seq cst fence会插入fence保证全序,而seq cst load可能不会保证bottom的顺序?这里有两种情况,当我们写入bottom锁定一个值的时候,如果有其他的线程在stealing,他可能看不到我们的写入,这时候他会增加top到大于newb。

20220607174655

这个图是本地线程和远端线程争用一个元素的情况。争用一个元素的话两个人会都通过CAS执行,所以不会出错。

考虑这种情况

20220607175943

这时候线程A读到的是最新的top,然而在他load top之后,出现了很多stealing,将队列偷空。然而我们基于过期的top决定把过期的元素pop出来。

这种情况会被seq cst fence否认掉。原因是并发的两个steal线程一定会有一个人失败,然后他会重新读取bottom,这时候由于我们在store bottom和load top之间有fence,steal线程一定会读到这个bottom。从而结束stealing。

当然也有可能是两个steal线程成功了。这个时候说明我们的store bottom在他们load的下面。那么这时候我们的load top一定可以读到最新的值,从而保证我们可以正确返回。

所以可以看到,决定的点其实在我们的load pop。如果load到了最新的值,那么我们的pop就会失败。因为没有元素了。如果load到了stale的值,那么由于fence的存在,其他的线程一定可以看到store bottom,从而提前退出,让pop可以成功。

20220607182107

加上fence可以这么看

实际上如果seq cst fence是通过mfence实现的,那么steal中的fence就不需要了。因为他本身的load就是acquire,我们只要保证看到pop中写入的bottom即可。

详细可以看这里issue

用来确保steal线程可以看到新的bottom。如果他看到了,那么steal线程会失败。如果他没看到,那么pop线程一定会看到新的top,那么pop线程会失败。

新的疑问?貌似这里的fence只用来保证bottom被全局可见,因为top的更新是seq的,所以我们的load是可以读到最新值的。这样的话,为什么不简单的通过acquire release来同步呢?答案是top的store在seq cst下可能实现的是release语义,所以我们需要mfence来同步才能让top加载到最新的值。

标签: 暂无
最后更新:2022年6月7日

sheep

think again

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 heavensheep.xyz. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS