令人迷惑的 context

很长一段时间,我都搞不懂 Go 的 context package。最近我读了两篇文章:

终于理解了 context 是在干什么,以及为何它让人费解。

从设计的目的上说,context 是用来控制 goroutine 的,确切地说,是为了当你需要的时候,能干掉一个 goroutine。比如你用 go someFunc() 创建了成千上万个 goroutine,一切都很美好。突然你不想让它们继续执行了,这时你发现,糟了,你根本不知道这些 goroutine 放在哪,更别说控制它们了。不像其它语言用多线程时往往有个 Thread object,goroutine 并没有保存在某个变量里。怎么办?

一. 如何让 goroutine 从内部退出?

办法是每次创建 goroutine,都传进去一个 context,把函数定义成这样

func someFunc(ctx context.Context) {
  select {
  case <-ctx.Done():
      return
  }
}

这个模板包含了实现 goroutine 控制的必要元素。首先有一个 context.Context 类型的参数 ctx,它有一个最重要的方法 Done()case <-ctx.Done(): 本质上就是在做这样一个判断 if ctx.goroutine_should_be_cancelled(),如果是,那它就进入这个 case 并且 return,结束当前 goroutine 的执行,否则进别的 case 继续执行。Done() 的实现机理在这里并不重要,我们只需要知道它在做判断即可。( 其原理是返回一个 read-only 的 channel,这个 channel 在 ctx被 cancel 的时候关闭,而读被关闭的 channel 会直接返回一个 zero value 而不会阻塞。)

这里用两个例子展示 case <-ctx.Done(): 和别的控制逻辑(比如循环,别的 channel)是怎么结合使用的,比较简单所以就不解释了。

Example 1

func someFunc(ctx context.Context) {
    for {
        innerFunc()
        select {
            case <-ctx.Done():
                // ctx is canceled
                return
            default:
                // ctx is not canceled, continue immediately
        }
    }
}

Example 2

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

理论上说,不传 context 而是传 channel 来控制也做得到,但为每个 goroutine 都创建一个 channel 并不符合 channel 的用法。

二. 上层如何告诉 goroutine 该退出了?

现在我们已经知道 <-ctx.Done() 用来在 goroutine 内部用来判断当前 goroutine 是否应该退出。那么外界该如何控制?有两种方式,要么是手动 cancel,要么是设定条件自动 cancel。

先看手动 cancel:

ctx, cancel = context.WithCancel(context.Background())

WithCancel 返回一个被包装过的 context,以及一个 cancel 函数。只要把 ctx 传进 goroutine,在合适的时候调用 cancel() 就可以告诉 ctx,这个 goroutine 应该退出了,从而进入 case <-ctx.Done():

自动 cancel 可以设置两种条件:1. 设置一个 deadline,时间达到或超过 deadline 则 cancel;2. 设置一个 timeout,超时则 cancel。函数是 WithDeadlineWithTimeout,用法和 WithCancel 一样。

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

三. Context For Pythonista

自然我们要看一下其它语言对与并发是如何控制的。比如,在 Python 里人们怎么让一个线程停止的?

Is there any way to kill a Thread in Python?

首先必须要说明,一般情况下不应该去试图干掉一个线程。当你需要这么做时,最简单的方法是在线程类里加入一个 threading.Event object 成员变量,之后在线程里去定期检查 event.is_set()。甚至于你都可以直接用一个 Boolean。对于 Java 也是类似的方法(当然实际上 Java 有更暴力的 Thread.interrupt(),不过我们假定用户想自己控制线程的退出)。

观察一下会发现,不管是 Python 里继承了 threading.Thread 的类,还是 Java 里实现了 Runnable 接口的类,它们都是个类,从而你可以在其实例中添加任何需要的变量。而且,由于每个线程都是一个实例,你可以去调用上面定义的方法,比如 terminate(),来设置之前提到的 Boolean 的值。

Go 就不行了,由于没有“goroutine object”,必须要以参数的形式把一个实现控制功能的载体也就是 context 传进去。再仔细观察一下,case <-ctx.Done(): 实现了 wait,和 Python 的 threading.Event.wait() 功能完全相同——毕竟这种等待的机制非常有用。

四. 为什么 context 看起来很奇怪?

context 奇怪的根源,在于它混合了两种不同的目的,更确切地说,是因为加入了 WithValueWithValue 非常好理解,就是外界在 context 上 set 一个 (key, value) pair,然后在 goroutine 内部读取。比如

ctx := context.WithValue(context.Background(), "key", "value")
go func(ctx) {
   value := ctx.Value("key")
}()

KV 存储嘛,谁都知道在干嘛。但是我们不要忘了 context 的主要用途是控制 goroutine 的退出。于是就很奇怪了,明明是控制 goroutine 退出的 indicator,为什么又能当成 KV 存储来用呢?这个问题困扰了许多人,以至于 Dave Cheney 专门写文章吐槽:《Context isn’t for cancellation》,他的观点是,context 顾名思义应该就是做存储用的,用来 cancel 才奇怪,所以应当:

I propose context.Context becomes just that; a request scoped association list of copy on write values.

那篇文章还有个有意思的点,就是当前的 context 其实并不能做到完美的控制,不过这里暂且不提。总之,这个混合了两种目的的设计,不论是谁都觉得奇怪。我猜想,设计者的初衷应该是不想让人们传两个变量,索性都塞进 context。

WithValue 相当于 Python 里的 threading.local,但显然,没人会把 threading.localthreading.Event 合成一个东西来用。

我的百合乃工作是也!

突然想写一下这部漫画了《私の百合はお仕事です!》。

可能有人知道,这部作品是《Comic百合姫》杂志上连载的大热作品之一(以下简称百乃工)。说起该杂志,不得不提一下最近动画化的两部作品,《Citrus》和《捏造トラップ -NTR-》。当初动画化消息放出来时还是非常轰动的,我也在知乎回答了一下相关问题。捏造的动画已经完结,查不到圆盘销量怎样,不过至少吸引了众多眼球,想必杂志的知名度也上升了。正如回答所言,当时我是不太感冒这两部的。现在一年过去了,Citrus 还是蓄力中的状态,倒是已经完结的捏造,我觉得还挺不错的——最后几话由真坚定信念去找萤,以及萤从最开始的不相信到最后完全把自己托付给由真,让人非常感动。整体来看,捏造算是前期铺垫后期爆发的成功案例,就看 Citrus 能否复制了。

说回百乃工。动笔之前我才发现,上次写的漫画评论居然也是未幡的作品:《周六的咖啡当与犯人共饮》,看来我的确是喜欢他(其实不确定性别)的风格。查了下过往作品,《キミイロ少女》我看过,是那种意犹未尽点到为止的短篇集,和咖啡那篇类似。除此以外也都是些短篇或者与人合作,也就是说百乃工是他的第一部中长篇,这样就显得更加难得了。下面来说一下我觉得这部作品主要好在哪(连载至14话)。

  1. 题材新

    最大的闪光点当然要放在第一个讲。想当初百合姬一口气上了十部新连载

    然而第一话后,还能让我有兴趣往下看的也就三四部。其中最让人期待,或者说唯一让人眼前一亮的,就属这部百乃工了。本来当时就想写篇文章来吹的,后来怕吹早了,总是想再等等,一等就等到了14话。今天,我终于可以说,百乃工是一部优秀的作品,至于再上一个台阶成为名作,也不是没有可能。

    为什么它能给人眼前一亮的感觉呢?因为这个题材完全没人画过。百乃工的故事,简单来说就是有一家咖啡店,卖点是女服务生们有个虚拟的人设:莉贝女子学园的学生,

    漫画讲的就是以大小姐学校学生之姿态,在咖啡店打工的女生之间的故事。

    这个题材的取巧之处在于把两种常见题材(大小姐学校、咖啡馆打工)融合起来,让人耳目一新。一开始我以为百乃工要 diss 以往的大小姐学校题材漫画,“所谓优雅的大小姐学校,其实都是演给你们看的啦”这样,便觉得太有意思了。然而,事实是我完全低估了把两种题材结合起来的效果,因为漫画比我预想的还要有趣。

  2. 有趣

    在学园百合咖啡店里展开的,虚实交错的百合故事开演!

    作者在第二话的扉页上写了这么一句话,可以说直接解释了故事的核心:“虚实交错”。这部漫画最有趣的地方,正是在于在虚拟的莉贝学园中的故事和现实中人物关系之间相互产生影响。举个例子,说起大小姐学校,姐妹制度是个常见的设定(现实中也存在实行这种制度的女校)。这种天生就带着戏剧性的制度自然是要被漫画所用的:

    这幅图是主角组二人成为姐妹的场景,我觉得可以很好地体现出之前提到的“相互影响”。姐妹按理说应该是关系融洽的,但为什么矢野(黑长直的这位)会说“我最讨厌你了”呢?看到这里人们自然会有疑问。答案在后续的几话中逐渐揭晓,原来这俩人是认识的,以前也发生过一段故事。过多的剧透就不讲了,总之这种虚实交错乃是漫画最有趣的地方。

    另一个在开始阶段吸引人看下去的地方是女主白木阳芽的性格,虽然这一点现在已经不是剧情冲突的关键了。女主是个极其擅长装可爱的人,之所以加入咖啡馆当服务生,也是认为这样会有更多人意识到自己的可爱。这种性格在二次元里也不算少见了,但每次看到还是觉得很有意思。

    鉴于百乃工的上述特质,我认为即便拿给普通人看,它也绝对称得上是一部有趣的漫画。若要挑一部百合姬作品动画化,百乃工是毫无疑问的首选,或者不如说我相信它未来可以动画化。

  3. 剧情节奏好

    剧情把控需要功力。百乃工给人的第一印象是设定新颖有趣,然而这样还不够,因为设定好叙事烂的作品实在太多了。我能想到比较著名的例子就是奥浩哉,《犬舍》和《杀戮都市》 的作者,设定都很有新意,但叙事平庸。目前来看,百乃工保持着非常好的叙事节奏,每一话都有看点。当然了,这和漫画仍处于前期有关。前期总是好画的,因为每个人物都有故事可以讲,而讲完每个人之后怎么推动剧情,这才是最难的。

    由于是虚实交错的故事,在叙事方面可以看做有两条线,一条是她们作为莉贝女子学园学生打工时的事,另一条是她们在打工之外的生活。百乃工不仅平衡了两条线的叙事,还让两台线的故事之间产生了化学反应,非常值得称赞。

  4. 视角平衡

    之前对 Citrus 的评论里,我提到 Citrus 之所以不好看,主要原因在于女主之一的芽衣完全不给内心描写。后来有人说,其实内心描写不一定是正面的,也可以是侧面的,我觉得也有道理。不过就算是侧面描写,也至少得以当前想展现的人的视角来画。现在阅读漫画时我会有意识地去关注作者有没有转换视角。根据我的经验,视角越是平衡的作品,往往质量越高。

    百乃工的人物不多,但也不少,不是那种除了主角组其它人都可以忽略的漫画。在这种情况下,它的视角切换,我认为做得是非常好的——除了打酱油的店长,四个主要人物都给足了视角。平衡的视角带来的是读者对于人物内心的体察,百合作为主打内心描写的题材,只有让读者充分了解主要人物的心理,才有阅读乐趣。

    当然不是说百乃工就无可挑剔了。说到视角和心理描写的巅峰,那肯定非森永みるく莫属了。如果你观察森永老师的分镜和叙事,会发现她笔下的人物内心描写,不是简单地弹个气泡文字框出来,生硬地说她想什么什么,而是直接融进了叙事。还是用之前写过的那张图(这图我可以吹一辈子)

这种极强的代入感,看了这么多漫画,依然没见有人比得上。不过这大概是个人风格的问题了,只能说,森永みるく天生就是应该来画百合漫画的。

删掉了若干失效友链

刚刚删掉了若干失效的友链。对不再维护博客的朋友们,我感到非常遗憾。写博客的意义是什么?在我看来,最主要的是记录自己的想法。笛卡尔那句名言我们都知道,“思想”乃是唯一确实存在的东西。然而每当我去翻阅旧文章,总会不禁想,这真的是我吗——如果当下的我已经写不出或者不会去写这篇文章,那以前写出它的那个人,真的可以称之为“我”吗?应该是不能的。然而,毫无疑问的是,那个人与当下的我又有紧密联系,假如什么都不做,那他也就消失了。好在,还有文章,我还可以回忆起,当时的那个“我”。

P.S. 如果有朋友想加友链,说一下就好,基本上我还没拒绝过。


top