Clojure

Go 块最佳实践

一般建议

在不等待回复的情况下发送消息,很容易想到以下做法

(go (>! c 42))

虽然 go 块很便宜,但也不是完全免费。因此建议使用

(async/put! c 42)

go 最终还是会调用 put!,所以实际上没有缺点。

此外,如果代码在回调内部被调用,并且你希望尊重背压,可以使用递归函数和 put! 来尊重背压,这相当容易。

(defn http-call
  "Makes an async call to a web browser"
  [url callback] ...)


(def urls [url1 url2 url3])

(defn load-urls
  "Spools the results of loading several urls onto a channel.
   does this without creating temporary channels or go blocks"
  [urls out-c]
  (http-call
    (first urls)
    (fn [response]
      (put! out-c response (fn [_] (load-urls (next urls) out-c))))))

(load-urls urls response-chan)

在这个例子中,我们有一些干净的互操作代码,它允许我们开始在应用程序中使用通道,而无需创建大量的通道或 go,只为了在创建后不久就处理掉它们。

请记住,尊重背压很重要。core.async 中的一般原则是无界队列很糟糕,并且待处理的 put 数量有限(目前为 1024)。另一个选择是使用一个带有缓冲区的通道,该缓冲区始终立即接受 put,例如 dropping-buffersliding-buffer

go 块中不支持的构造和其他限制

go 宏在函数创建边界处停止翻译。这意味着以下代码将无法编译,或者可能只是抛出运行时错误,指出 <! 在 go 块之外使用。

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

这是需要记住的一点,因为许多 Clojure 构造在宏内部创建函数。以下是一些代码示例,它们无法按预期工作

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

但是,其他 Clojure 构造,例如 doseq 不会在内部分配闭包

; This works just fine
(go (doseq [c cs]
      (println (<! c)))

不幸的是,目前还没有一个好方法来判断给定的宏在 go 块内部是否按预期工作,除非查看源代码或测试宏生成的代码。

为什么是这样?

对于“为什么 go 块翻译在函数创建处停止?”这个问题,最好的解释基本上归结为类型问题。检查以下代码片段

(map str [1 2 3])

我们可以很容易地看到这会生成一个字符串的 seq,因为 str 的输出类型是字符串。那么 async/<! 的返回类型是什么?在 go 块的上下文中,它是一个从通道中获取的对象。但是 go 块必须将它翻译成对 async/put! 的停车调用。async/<! 的返回类型实际上应该被认为类似于 Async<Object>Promise<Object>。因此 (map async/<! chans) 的结果类似于“待处理通道操作的 seq”,这完全没有意义。

简而言之,go 宏在没有一些认真工作的情况下无法执行这些操作。其他语言,例如 Erjang,允许通过翻译整个 JVM 中的所有代码来实现此类构造。这是我们希望在 core.async 中避免的事情,因为它会使事情复杂化,并导致一个库的逻辑感染整个 JVM 的代码。因此,我们选择了实际的妥协方案,翻译在看到 (fn [] …​) 时停止。

原始作者:Timothy Baldridge