Clojure and go blocks: How to stop your program from exiting
bretthancox
Posted on July 12, 2019
This is my first post and probably on a topic done to death, but it's a topic I tripped over when first using the core.async features of Clojure.
Clojure's core.async uses go blocks. Wrap something in a go block and it will happen asynchronously. Simple, right?
What tends to happen when you first try is you write something like this to see if you get the principle:
(defn counter [start end]
(loop [count start]
(if (> count end)
1
(do
(println count)
(recur (inc count))))))
(defn -main
"I don't do a whole lot ... yet."
[& args]
(go (counter 0 100)))
Aaaand nothing happens. Maybe you're lucky and it counts to "1", but then the program exits. Why didn't it finish?
Well, it did. When you put something inside a go block, you tell Clojure you want it to occur in another thread. The program pushes the actions inside the go block to another thread and then returns to the current thread immediately.
That last part is critical. The main thread contains nothing after the go block. So the program is done and exits. There is nothing left to do. The only way the above example would count to 100 is if the main thread had enough work to do that the other thread could complete.
So, how do you make sure this simple example completes?
First, it is important to understand that any go block makes a channel. I won't go into detail on channels, but anyone learning the core.async library has probably seen something like this:
(def my_channel_name (chan))
There are other ways to define a channel, but generally you would use chan
, whether in a def
statement or in a let
or other binding.
A go block is also a channel. From the clojuredocs, go "...Returns a channel which will receive the result of the body when completed..."
Whatever you put inside the go block, its return value is placed on a channel that is the go block itself. In my example, when the if
statement is true, the go block will exit with a value of 1
. It doesn't really matter what that value is as I am not using it, but the presence of the channel and something being placed on it are the key to making sure your program concludes after the go block is finished.
Core.async has multiple methods for taking values from a channel, but we will be using <!!
in this example. This is one of two similar looking functions in core.async with similar purposes. The other is <!
.
<!
can only be used inside a go block. It is a non-blocking take for the main thread. Non-blocking just means the program doesn't wait for it to take anything from the associated channel.
<!!
is a blocking take and can be used outside a go block. If there is nothing for it to take (i.e. nothing on the channel it is taking from) then it blocks the thread in which it appears.
So, before we update our example, let's recap:
- Go blocks take place on other threads than our main thread.
- The main thread finishes because it has no more work to perform.
- A go block acts like a channel that will provide the return value of anything that takes place inside it.
-
<!!
blocks a thread until it can take a value from a channel.
So, we can make a very simple change to our example:
(defn counter [start end]
(loop [count start]
(if (> count end)
1
(do
(println count)
(recur (inc count))))))
(defn -main
"I don't do a whole lot ... yet."
[& args]
;;(go (counter 0 100))) ;;not used
(<!! (go (counter 0 100)))) ;;new code
And now the program prints 0 to 100.
The addition of the <!!
blocks the main thread from completing until it can take a value from a channel. In this case, the channel is our go block and the counter function inside doesn't return a value until it finishes counting from start
to end
.
Another option is to use a channel to tell the main thread when work is finished in other threads.
When a channel is explicitly closed using close!
it returns nil
as its last value. Since nil
is a false value, the following code can check for the closing of a designated "last action" channel by monitoring for a false return value:
(defn -main
"I don't do a whole lot ... yet."
[& args]
(let [close_on_last_action_chan (chan)]
(go-loop [counter 1]
(if (> counter 1000)
(close! close_on_last_action_chan)
(recur (inc counter))))
(while (<!! close_on_last_action_chan))))
Just make sure you use this channel only to monitor for conclusion of the program. Putting other 'false' values on this channel could end the program prematurely.
Conclusion
These are contrived examples. There is absolutely no value in doing simple incr
loops in a go block. What it demonstrates is the need to "capture" the main thread if it might conclude before async events complete.
I hope that this was helpful to someone. I have little doubt there are better ways to do what I've described above, but if nothing else this approach helped me with some of the issues I experienced understanding the core.async library.
Thanks for reading!
Posted on July 12, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 30, 2024
November 18, 2024