Clojure core.async通道如何清理?

8
我第一次接触Clojure core.async,并正在浏览Rich Hickey的这个优秀演示文稿:http://www.infoq.com/presentations/clojure-core-async 我有一个问题,关于他在演示文稿末尾展示的例子:
根据Rich的说法,这个例子基本上试图为特定查询获取Web、视频和图像结果。对于每个结果,它会并行尝试两个不同的来源,并仅拉出最快的结果,整个操作不超过80毫秒,所以如果我们无法在80毫秒内获取图像结果,我们就放弃了。'fastest'函数创建并返回一个新通道,并启动两个go处理程序来竞速检索结果并将其放在通道上。然后我们只需从“fastest”通道中取出第一个结果并将其放入c通道即可。
我的问题是:在我们获取它们的第一个结果后,这三个临时的未命名的“fastest”通道会发生什么?可能仍有一个Go进程正在等待将第二个结果放到通道上,但没有人在听,因此它实际上永远不会完成。由于通道从未绑定到任何东西,似乎我们没有任何方法再次使用它。Go进程和通道会“意识到”没有人再关心它们的结果并清理自己吗?还是我们在这段代码中实际上只是“泄漏”了三个通道/ Go进程?
3个回答

4

没有泄漏。

已停止的go与它们尝试执行操作并且除此之外没有独立存在的通道相连。如果其他代码对某个go停留在上面的通道失去兴趣(注意:如果它停留在alt! / alts! 上,一个go可以同时成为许多通道的放置者/取出者),那么它最终将与那些通道一起被垃圾回收。

唯一需要注意的是,为了进行垃圾回收,go实际上必须先停留。因此,任何一直在循环中执行操作而从未停留过的go<! / >! / alt! / alts!)实际上将永远存在。但很难无意中编写此类代码。


嗯,好的。你和莱昂给出了两个相互矛盾的答案。你能提供你观点的参考吗? - Ord
是的,请链接到实现细节。另外,请解释一下这在上面的代码示例中如何工作。 - Leon Grapenthin
假设 c 阻塞了 put 操作,fastest 进行了第二个未被消费的 put 操作。在上述代码示例中,cfastest 返回的 channel 何时被垃圾回收? - Leon Grapenthin

3

除了注意事项和例外情况,您可以在REPL上测试JVM的垃圾回收。

例如:

(require '[clojure.core.async :as async])
=> nil

(def c (async/chan))
=> #'user/c
(def d (async/go-loop [] 
         (when-let [v (async/<! c)] 
           (println v) 
           (recur))))
=> #'user/d

(async/>!! c :hi)
=> true
:hi        ; core.async go block is working

(import java.lang.ref.WeakReference)
=> java.lang.ref.WeakReference    ; hold a reference without preventing garbage collection
(def e (WeakReference. c))
=> #'user/e
(def f (WeakReference. d))
=> #'user/f

(.get e)
=> #object[...]
(.get f)
=> #object[...]

(def c nil)
=> #'user/c
(def d nil)
=> #'user/d
(println "We need to clear *1, *2 and *3 in the REPL.")
We need to clear *1, *2 and *3 in the REPL.
=> nil
(println *1 *2 *3)
nil #'user/d #'user/c
=> nil
(System/gc)
=> nil
(.get e)
=> nil
(.get f)
=> nil

我刚才发生了什么?我设置了一个go块并检查它是否正常工作。然后使用WeakReference观察通信通道(c)和go块返回通道(d)。然后我删除了所有对c和d的引用(包括由我的REPL创建的*1*2*3),请求进行垃圾回收(并且幸运的是,System.gc Javadoc不能做出强有力的保证),然后观察到我的弱引用已被清除。
在这种情况下,一旦与所涉及的通道相关的引用被删除,通道就会被垃圾回收(无论我是否关闭它们!)

1
假设由fastest生成的通道仅返回最快查询方法的结果,然后关闭。
如果产生了第二个结果,则您的假设可能成立,即fastest进程泄漏。它们的结果从未被消耗。如果它们依赖于所有结果都被消耗才能终止,那么它们将无法终止。
请注意,如果在alt!子句中选择通道t,也可能发生这种情况。
通常修复此问题的方法是在最后一个go块中使用close!关闭通道c。发送到已关闭的通道的值将被丢弃,生产者可以终止。
问题也可以通过实现fastest来解决。在fastest中创建的进程本身可以通过alts!timeout进行放置,并在一定时间内未消耗生成的值时终止。
我猜Rich在幻灯片中没有解决这个问题,而是选择了一个不太冗长的示例。

哦,好的。现在我从你和Michal那里得到了两个相互矛盾的答案。你能提供一些支持你说法的参考资料吗? - Ord
或许Michal关于go blocks的陈述是正确的。但我们不知道fastest的实现是否使用了go blocks。如果它产生线程,在并发搜索查询的情况下很可能会使用>!!进行阻塞放置,这些被抛弃的线程将永远留在线程池中,直到JVM在足够的请求之后死亡。 - Leon Grapenthin
这是示例代码中的虚拟实现,只是调用 (go ... (sleep ...) done),因此它对回答这个使用问题没有参考价值。 - Arthur Ulfeldt
严格来说,Michals的答案是正确的。我不知道提供了一个虚拟实现。 - Leon Grapenthin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接