一般建议
像这样发送消息而不等待回复的写法非常有吸引力:
(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 [] ...)
时停止展开。