Lua5.3 GC源码阅读(5)

这篇内容并不是上篇提到的关于弱表实现的分析。而是最近有位同学与我探讨了一些Lua的GC时,发现了两个以前从没注意过的智慧闪光点。

首先是luaC_fullgc的实现.


 #define keepinvariant(g)	((g)->gcstate <= GCSatomic)

static void entersweep (lua_State *L) {
  global_State *g = G(L);
  g->gcstate = GCSswpallgc;
  lua_assert(g->sweepgc == NULL);
  g->sweepgc = sweeplist(L, &g->allgc, 1);
}

void luaC_fullgc (lua_State *L, int isemergency) {
  global_State *g = G(L);
  lua_assert(g->gckind == KGC_NORMAL);
  if (isemergency) g->gckind = KGC_EMERGENCY;  /* set flag */
  if (keepinvariant(g)) {  /* black objects? */
    entersweep(L); /* sweep everything to turn them back to white */
  }
  /* finish any pending sweep phase to start a new cycle */
  luaC_runtilstate(L, bitmask(GCSpause));
  luaC_runtilstate(L, ~bitmask(GCSpause));  /* start new collection */
  luaC_runtilstate(L, bitmask(GCScallfin));  /* run up to finalizers */
  /* estimate must be correct after a full GC cycle */
  lua_assert(g->GCestimate == gettotalbytes(g));
  luaC_runtilstate(L, bitmask(GCSpause));  /* finish collection */
  g->gckind = KGC_NORMAL;
  setpause(g);
}

在luaC_fullgc中,主要的意图是首先尽可能快的结束执行到一半的GC循环,然后从头开始一轮新的GC循环,以达到fullgc的语义。

那么怎么快速结束当前未结束的循环呢,Lua采用了一个很有智慧的方法,那就是快速进入sweep阶段(即如果当前还没有处理sweep阶段,就直接进入sweep阶段)。那么我为什么说这是一个很有智慧的方式呢,这是因为不论在整个GC状态机的任意一个状态进入sweep阶段,整个GC状态都不会出错,这是我以前没有意识到的。

先来看一下整个GC状态机全部状态:

 #define GCSpropagate	0
 #define GCSatomic	1
 #define GCSswpallgc	2
 #define GCSswpfinobj	3
 #define GCSswptobefnz	4
 #define GCSswpend	5
 #define GCScallfin	6
 #define GCSpause	7

如果将一个GC循环按照标记和清除来区分,很显然GC循环的开始在GCSpropagate, 结束在GCSpause。整个GC循环被GCSatomic状态分为标记和清除两个大阶段。

为了更清楚的看清整个GC循环,我们将每个GC循环概括为GCMark和GCSweep两个阶段,并把几个GC循环连起来看。

GCMark1, GCSweep1, GCMark2, GCSweep2,GCMark3, GCSweep3…

由于atomic函数会改变当前GC循环的白色值,因此GCSweep1,和GCMakr2阶段拥有相同的白色值,而GCSweep2,GCMark3拥有相同的白色值。

从sweeplist函数可以看出GCSweep2只会清理GCSweep1和GCMark2阶段的白色对象。并把所有存活对象都变成当前GC循环的白色值。

假如我们在执行GCMark2过程中调用了luaC_fullgc导致跳过了atomic阶段的执行,那么就不会产生更换GC循环白色值的行为,这样GCSweep1,GCMark2,GCSweep2,GCMark3都使用了相同的白色值来执行标记清除逻辑。由于GCSweep1已经清除过GCSweep0和GCMark1阶段产生(包括由黑变白)的白色对象,那么GCSweep2的作用其实只剩下“把所有在GCMark2阶段标记过的对象都变白”。相当于撤销了GCMark1的作用。而从GCMark3开始就能执行一轮全新的GC循环。

这就变相达到了最快结束当前GC循环的目的。


另一个就是luaC_barrierback的实现.

 
 #define luaC_barrierback(L,p,v) (  \
	(iscollectable(v) && isblack(p) && iswhite(gcvalue(v))) ? \
	luaC_barrierback_(L,p) : cast_void(0))

void luaC_barrierback_ (lua_State *L, Table *t) {
  global_State *g = G(L);
  lua_assert(isblack(t) && !isdead(g, t));
  black2gray(t);  /* make table gray (again) */
  linkgclist(t, g->grayagain);
}

这个函数其实在上篇分析中也有提到过。但那是在GCMark阶段时分析的luaC_barrierback的作用。我从来没有注意过如果在GCSweep阶段,触发了luaC_barrierback会怎么样。

在GCSweep阶段会触发luaC_barrierback么?答案是肯定的。因为sweeplist是增量执行的,不可能一下子就把所有黑对象变白。如果这时操作黑对象,就会触发luaC_barrierback。

那么在GCSweep阶段,如果触发了luaCbarrierback,那么相关黑对象会被变成灰对象,然后被挂在g->grayagain链表上。同时如果一个对象触发了luaC_barrierback, 说明他还没有对sweeplist处理过,也就是说最终他一定会被sweeplist函数变成白色。

这时就会存在一种情况,一个对象被挂在g->grayagain链表上,但是它是白色。这就是其中的智慧闪光点,虽然一个白色对象挂在g->grayagain, 但是他不会有副作用。因为在restartcollection函数中的 g->gray = g->grayagain = NULL语句会将整个grayagain链表清空。同时虽然白色对象的gclist字段依然有无效值,但是在下次插入gray或grayagain之前,会对此对象的gclist字段重新赋值,因此也不会有任何副作用。

纵观这两个例子可以发现,sweeplist本来是为了GCSatomic阶段之后清除对象而生,而luaC_barrierback本来也是为了在GCMark阶段保持GC循环不变式而生。但是他们在这两个场景之外,依然有良好的适用场景。这得益于良好的抽象,我有时甚至都在想,这也许已经是人类智慧的极限了。



发表评论