mluví inkoustem

Poslední články

Introduction

So, you want to do asynchronous programming in Clojure. You've searched on the internet and seen lamina and pulsar, but there's still some undescribable urge to examine what is the "official" solution to all those channels and whatnot.

Let's see it then! This is going to be a primer of sorts. Not answering all the questions, but getting your feet wet.

I'm going to walk you through the exciting world of Rock Paper Scissors! We will model the situation where two players get into a game, make their moves and a judge will decide who wins.

It's very simplistic but there's a lot of space for you to experiment further: room with many (3+) players making games randomly, tournament where players advance the bracket, games of best-of-3, etc.

So, let's dive in!

project.clj

Our first struggle happens right at the project.clj.

Because core.async isn't yet pushed to clojars or some standard maven repository, you either have to install it to your own local maven repository (what? I don't even have maven installed), or let Leiningen know that you want to fetch it from some other repository. And that's what we're going to do.

It's sequential and makes us define the exact order of operations. In this little example it doesn't seem that much of a fail, but it's not optimal. Also there's no immediately obvious way to make this behave concurrently. Maybe futures?

Anyways, let's see what does core.async bring to the table.

chan, >!!, <!!

Here you see the three main functions you'll use.

(chan) ; Unbuffered channel.
; The sender and receiver will wait (block) for each other.
(chan 5) ; Channel with 5 slots. FIFO.
; If empty, the receiver will block.
; If full, the sender will block.
(chan (buffer 5)) ; The same as above.
(chan (dropping-buffer 5)) ; If full, the new value will be ignored.
; Oh, and the sender won't block.
(chan (sliding-buffer 5)) ; If full, the oldest value will be dropped.
;---------------
(>!! ch :val) ; Put :val into the channel.
(<!! ch) ; Read from the channel.

If the exclamation marks confuse you, read them this way:

The bangs are the channel.

When you do >!!, you point at the channel - you're giving it something.

When you do <!!, it's pointing at you - it's giving you something.

Now, let's redefine the player. He finally has somebody/something to tell his decisions to! He knows he will tell it to the judge using the channel. (Think about it as a mailbox.)

I want you to pause on the fact that if we run on a single thread and we call the judge before both players have made their decisions, we get into a deadlock. The judge will wait for the second hand (or both of them) and of course won't be getting it. More on that later.

Can you see the change here? We don't plumb the players' hands to the judge - the players put their decisions to the channel and the judge gets them from it. It's their decision, not the game's (whatever that means).

We've moved the reasoning about the message-passing from the play function to the individual functions - players and the judge. The only thing we still have to think about here is giving them all the same channel ;)

Wins

Separation of concerns. The possibly concurrent code is in composable, linear units. You can reason about the different parts independently.

We have to think about the order of operations. If we call the judge before the players, we get into a deadlock. Imagine an use case where the judge would put the results back into the channel and the players would read it and say "Yay" or "Oh no!" depending on whether they won or lose. You can't do that right now.

thread

So, we want to avoid the deadlocks. The solution is to either be able to do more things at the same time, or to be able to jump between all the different parts that are waiting for something. We will examine the first possibility first and then the other.

(thread ...) ; Executes the body in another thread and returns immediately.
; Much like futures, don't you say?
; Returns a channel which will contain the result when completed.

So, how does our usage change? It's simple, the only thing that changes is our play function. We don't call the players and judge sequentially, we put them on different threads!

If you're puzzled by the <!! on the last line, remember that thread returns a channel with the result of the body passed in.

All the <!! and >!! functions are still blocking! They're just on another threads so right now it's possible to change the order of the calls. We can call the judge as the first one, and he won't deadlock us to eternity.

There's just one little problem - prints from different threads don't show in your REPL. Otherwise I'd gladly do something like this just to show you we can change the order now and not get a deadlock:

This creates illusion of parallelism even in JavaScript, where it's not currently possible to program on more than one thread.

Let's look at it!

(go ...) ; Must wrap every use of <! and >!.
; Like thread, but works in JS.
; Also returns a channel with the result of the body.
(>! ch :val) ; Like >!! and <!!, the only difference is:
(<! ch) ; THEY DON'T BLOCK.

The go form doesn't span inside function calls. So if you do (go (something)) (to not block and let the other parts run too), you have to wrap the inside of (something) with (go ...) too.

The play function is pretty straightforward. We just replace every thread with go.

Wins

We finally don't have to think about the order!

Parallel when it can; on JS at least works!

Somehow gets printing into REPL right! ;)

Fails

Can't think of any.

Final discussion

Changed way of thinking

The core.async approach to all things async is not unlike Go's channels
or Erlang's actor model. I can see it modelling my thinking from Clojure's
more usual nouns describing the objects and verbs describing the computation
to the subjects (who - player, judge).

That seems very Erlang-y to me, and I like this kind of thinking - each
go block being one entity communicating via channels, if not mailboxes.

Is that just a consequence of thinking about concurrency, threads, etc.?

What are the channels?

A thing I don't know how to handle are the channels and how to think
about them - or at least how to name them.

If we stick to our game of RPS, what are they? Are they the "room"?
Are they some kind of "coordinator"? Or are they just "channels", really?
The "bus", "wire", ... "mailbox"? Just something connecting all these
otherwise independent entities?

It seems weird to me to just call them the "channel". But "game" in my
examples probably wasn't the best name either. The way we name things
shapes how we think about them. If there is a way that makes it all simpler
and more intuitive, I'd like to hear about it!

(Update: there will probably be some amount of English-speaking people trying to read this article. Sorry, it's in Czech. My next programming articles will all be in English. You can start with Rock Paper Scissors with core.async!)