clojure.core.async/go最佳实践

一般建议

像这样发送消息而不等待回复的写法非常有吸引力:

(go (>! C 42))

虽然go代码块资源消耗很低,但并非免费。因此建议使用async/put!代替:

(async/put! c 42) 

go底层实现仍会调用put!,所以这里没有真正的问题。

另外,如果要在回调中使用上述代码,同时要考虑背压(back pressure)时,使用递归函数配合put!就相当容易:

(defn http-call
  "异步网络请求"
  [url callback] 
...) 


(def url [url1 url2 url3]) 

(defn load-urls 
  "将多个网络请求的结果加载到一个channel里。
   不用创建临时channel或go代码块"
  [urls out-c]
  (http-call 
    (first urls)
    (fn [response]
      (put! out-c response (fn [_] (load-urls (next urls) out-c))))))

(load-urls urls)

这个例子演示了在应用中使用channel的整洁有效的方式,我们成功避免了使用大量短暂存活的channel或go代码块。

使用go的限制

go的展开停止于函数创建处。故而下面的代码要么无法编译,要么会抛出运行时错误,提示在go代码块之外使用了<!

(go (let [my-fn (fn [] (<! c))]
      (my-fn)))

请务必牢记这一点,因为许多Clojure自带的构造体(即函数或宏)内部创建了函数。知道了这点,下面的代码无法使用就不难理解了:

(go (map <! some-chan))
(go (for [x xs]
      (<! x)))

某些clojure构造体,如doseq不会在内部分配闭包:

;这样写没问题
(go (doseq [c cs]
      (println (<! c)))

不幸的是,目前还没有一种好的方法可以知道给定的宏是否可以按预期工作。你只能通过查看宏的源代码或者测试宏生成的代码的方式来判断。

为什么会这样?

为什么go的展开停止于函数创建处?根本原因归结为类型问题。请看如下代码片段:

(map str [1 2 3]) 

由于str的输出类型是一个字符串,我们可以很容易地看到这会产生一串字符串。那么async/<!的返回类型是什么?在go代码块内,它返回的是来自channel的对象。但是go代码块必须先将其转换为挂起调用的async/put!。所以async/<!的返回类型应该视为Async <Object>Promise <Object>这样的东西才对。因此(map async/<! chans) 的返回结果就像是“一个待处理channel的序列”,它根本没有意义。

简而言之,在没有大量工作的情况下,go宏是无法进行这类操作的。其他语言如Erjang通过转换整个JVM的所有代码来实现。但这正是我们想要避免的,因为它会使事情复杂化并导致一个库的逻辑污染整个JVM的代码。所以我们作出的妥协,go代码块在看到(fn [] ...)时停止展开。


本文译自Go-Block-Best-Practices

Comment