Clojure - Why does execution hang when doing blocking insert into channel? (core.async)
Asked Answered
P

2

9

Consider the following snippet:

(let [chs (repeatedly 10 chan)]
  (doseq [c chs]
    (>!! c "hello"))
  (doseq [c chs]
    (println (<!! c))))

Executing this will hang forever. Why is that?

If I do (go (>! c "hello")) instead, it works just fine.

Paisano answered 4/1, 2014 at 22:34 Comment(0)
L
15

To make an asynchronous put, use clojure.core.async/put!

(let [chs (repeatedly 10 chan)]
  (doseq [c chs]
    (put! c "hello"))
  (doseq [c chs]
    (println (<!! c))))

This works in this example as <!! will always unblock because of all necessary puts happening asynchronously. Notice the following things:

  • Blocking serves as a synchronization constraint between different processes
  • >!! and <!! block the main-thread. go routines run on the main thread but their code is modified via macroexpansion so that execution control is inverted and they can be parked/executed successively ordered by the laws of core.async channel blocking/buffering logic. This technique is commonly referred to as IOC (inversion of control) state machine.
  • ClojureScript has only one thread. Consequently its implementation of core.async doesn't even contain >!!/<!!. If you write code intended to be ClojureScript compatible, only take from channels within go-routines or dispatch values from them in higher order functions passed to take! and always do puts either in go-routines or use put!.

Is (go (>! ch v)) equivalent to (put! ch v)?

Yes but it's not the same. put! is an API wrapper around the channels implementation of the core.async.impl.protocols/WritePort put! method. Macroexpansion of (go (>! ch v)) ends up in the same method call happening but wraps it in lots of generated state-machine code to possibly park the putting operation and pause execution of the go-routine until a consumer is ready to take from ch (try to (macroexpand `(go (>! ch v))) yourself). Spawning a go-block to only do one asynchronous putting operation is kind of a waste and performs worse than calling put! right away. go spawns and returns an extra channel that you can take its bodies result from. This offers you to await completion of its execution which you didn't intend to do in your example (aiming at an asynchronous operation).

Loo answered 4/1, 2014 at 23:16 Comment(2)
Is (put! c) equivalent to (go (>! c))?Paisano
I edited my answer to hopefully provide more insight.Loo
P
5

That channel has no buffer, and >!! is blocking. Refer to the examples for this exact case. They spawn a second thread for this reason - to prevent blocking the main thread. Using a goroutine, as in your question, operates on a similar principle.

You can also create the channel with some buffer space - (chan 1)

Postoperative answered 4/1, 2014 at 22:41 Comment(3)
Ah, I forgot that puts are blocking as well. What do they block on? It's more intuitive for gets in that they block on values being present in a channel. But when you put something, what are you blocking on?Paisano
Your question title says "blocking insert", so you knew it at least subconsciously. :-) Basically, it will block until the channel has been able to receive the value. In this case, it means that there is a consumer ready to take the value immediately. I don't know implementation details, but conceptually a channel without a buffer acts as just a synchronization point. With a channel with a buffer, it will hold n items before blocking occurs.Postoperative
@MarkF By puts, do you mean >!/>!!? There is a put! function that does the same thing as those functions, but which does not block the calling thread.Jenninejennings

© 2022 - 2024 — McMap. All rights reserved.