Skip to content
Michael Peterson edited this page Jun 28, 2013 · 15 revisions

Go routines in Go can be thought of as spawning a process in the background, as you do with the & symbol on a Unix/Linux command line. But in Go you are not spawning a process or even a thread. You are spawning a routine or piece of control flow that will get multiplexed onto threads in order to run. Thus, go routines are lighter weight than threads. You can have more Go routines running than the possible number of natives threads supported in a process by your OS.

To create a Go routine you simply invoke a function call with go in front of it and it will run that function in another thread. In this aspect, Go's go is very similar to Clojure's future function.

func SayHello(name) {
  time.Sleep(2 * time.Second)
  print "Hello: " + name + "\n";
}
go SayHello("Fred");  // returns immediately
// two seconds later prints "Hello Fred"
;; the Clojure version of the above (almost)
(defn say-hello [name]  
  (Thread/sleep 2000)
  (println "Hello:" name))
(future (say-hello "Fred"))

Like go, Clojure's future runs the function given to it in another thread. However, future differs from Go routines in three important ways:

  1. future pushes a function to be executed onto a thread from the Clojure agent thread pool where it remains until the future is finished. Clojure futures are not a lightweight multiplexed routine. This is a limitation of the JVM environment and as far as I know there is not a way to emulate this aspect of Go routines on the JVM. (If any knows differently, please let me know!)

  2. The thread used by future is not a daemon-thread, whereas Go routines are daemons. In Go, when the main thread (as defined by the thread running through the main function) ends, any Go routines still running are immediately shut down. Thus, you can spawn a Go routine that never shuts down or is hung in a blocking operation and the program will still exit gracefully. This not true of Clojure future threads. The go-lightly library provides some utilities to assist with this.

  3. future returns a Future whereas go returns nothing. If you need to wait on a go routine to finish or deliver some value, you instead use a channel for communication (or, set some shared state for other thread to check, which is not idiomatic). With a Future you can wait on it to finish and return a value to you directly.

The go-lightly library provides two different go macros that internally invoke future. Both of them return the Future that is created, though it is typically ignored when writing Go-style concurrent programs.

When designing your concurrent programs in Clojure, think about whether you want to get the return value of the future and use that to coordinate threads. If so, then you probably don't need go-lightly. But if you want to spawn up "go routines" that will communicate via channels and treat those go routines like daemon threads, go-lightly facilitates that and makes it easy to do. See the wiki for detailed examples.

Versions of the go macro in go-lightly

go and stop

The go macro wraps Clojure's future macro, which means it launches a thread using the Clojure agent thread pool. These are not daemon threads, so the threads must either shut down on their own or be stopped. In regular Clojure code you would keep a reference to the future and call future-cancel on it to stop it when necessary.

The go macro keeps an inventory of launched "go-lightly routines" and when you call stop it will cancel all of them. This will throw an InterruptedException in the go routine so you may want to wrapper your go routine functions in a try/catch that ignores InterruptedException so it isn't printed out to the screen.

Or you can use the gox macro.

gox

The gox macro works just like the go (so you should call stop at the end). The additional feature it adds is that it wraps the whole go routine in a try/catch block that will:

  • ignore InterruptedExceptions (which are thrown when stop is called if the thread is still active)
  • print out any other exceptions to the screen.

So your code like this:

(go (be-excellent-to :each-other))

turns into:

(clojure.core/future 
  (try (be-excellent-to :each-other) 
     (catch InterruptedException ie) 
     (catch Exception e (.printStackTrace e)))))

This may not be what you want in production, but it can be very useful during development, since exceptions thrown in Clojure future are hidden unless you dereference the future. Since in go-lightly we tend to not deference the future, the exception is unseen and a source of confusion. Printing it out will help with debugging.

go&

Finally, the go& macro is an alternative that does not use the Clojure future/agent thread pool. Instead, each invocation of go& creates and starts a daemon thread. Calling stop will have no effect on threads created with this macro.

Otherwise, use it just like go.

Usage considerations

In Go you can spawn up millions of go routines and not worry about exhausting the thread space of the system. With a JVM-based language that is not the case. Modern JVMs no longer have green threads. They use native threads, which can be resource intensive and the number you can create is capped. A good short discussion on that is available on this stackexchange thread.

In his Google IO 2012 presentation, Rob Pike demonstrates this aspect of Go routines with a "chinese whispers" example. Let implement that chinese whispers example in Clojure and see what practical limitations I hit on my Linux system.

Chinese Whispers in Go

In the go-examples/src/chinese-whispers directory there is this implementation in Go:

package main

import "fmt"

func whisper(left, right chan int) {
    left <- 1 + <- right
}

func main() {
    const n = 100000
    leftmost := make(chan int)
    right := leftmost
    left := leftmost
    for i := 0; i < n; i++ {
        right = make(chan int)
        go whisper(left, right)
        left = right
    }
    go func(c chan int) { c <- 1 }(right)
    fmt.Println(<- leftmost)
}

This is an example to show off how light weight Go routines in Go are. He daisy-chains 100,000 go routines to each all connected by a point-to-point communication channel. Once they are all spawned up, he adds a message (an integer) to the 100,001st go routine - the "rightmost" channel, and easy hands it to its neighbor, incrementing the number along the way until the main thread receives it and prints out "100,001".

On my system when I run this it takes a quarter of a second to complete:

$ time ./chinese-whispers 
100001

real    0m0.247s
user    0m0.140s
sys     0m0.100s

Impressive.

(Note: If you want to run this, see the README in the go-examples directory for how to set up, compile and run the go-examples included in this repo).

Chinese Whispers in Clojure with go-lightly

In the clj-examples/thornydev/go_lightly/examples/whispers directory, I have implemented chinese-whispers in Clojure:

(ns thornydev.go-lightly.examples.whispers.chinese-whispers
  (:require [thornydev.go-lightly :as go]))

(defn whisper [left right]
  (go/put left (inc (go/take right))))

(defn whispers-main
  "build from left to right - all constructed before starts"
  [nthreads]
  (let [n (Integer/valueOf nthreads)
        leftmost (go/channel)
        rightmost (loop [left leftmost i (dec n)]
                    (let [right (go/channel)]
                      (go/go (whisper left right))
                      (if (zero? i)
                        right
                        (recur right (dec i)))))]
    (go/go (go/put rightmost 1))
    (println (go/take leftmost)))
  (go/stop))

Notice that I parameterized the number of threads, because we are going to hit limits that Go routines do not. So what are the practical limits?

Specs for my system:

  • OS: Linux 3.5.0-22 system
  • Java: Java 7 java version "1.7.0_10", Java HotSpot(TM) 64-Bit Server VM
  • Clojure: 1.4.0

First running from the REPL

user=> (require '[thornydev.go-lightly.examples.whispers.chinese-whispers :as w])
nil
user=> (time (w/whispers-main 1000))
1001
"Elapsed time: 142.353637 msecs"
nil
user=> (time (w/whispers-main 10000))
10001
"Elapsed time: 971.863342 msecs"
nil
user=> (time (w/whispers-main 20000))
20001
"Elapsed time: 3471.285835 msecs"
user=> (time (w/whispers-main 100000))
OutOfMemoryError unable to create new native thread  java.lang.Thread.start0 (Thread.java:-2)

Further narrowing shows it runs out somewhere between 30,000 and 40,000 threads.

I used jconsole to watch the JVM runs. When I try the 100,000 run, the jconsole thread window stops working so I can't display that but here's the output from the run with 20,000:

Heap and Thread Count from Clojure Chinese Whispers 20,000 threads

I also ran lein uberjar so I can run it from the java command line and control the heap size settings to see if that made a difference and it does not. By watching the heap with jvisualvm I confirmed this is not a heap space "Out of Memory Error" - it is what is says: unable to create new native threads. (BTW if you try this you are probably going to have to reboot since it hoses the OS pretty badly.)

Alternatives

So when doing Go-style programming on the JVM, we do have to be cognizant of the number of threads we spawn. Ideally, you will structure your application to have effectively reuse the agent thread pool that Clojure manages for us.

For example, I restructured the chinese whispers program to build from right to left on the fly, rather than pre-constructing everything left to right and keeping it all in memory in order to get to the end.

(ns thornydev.go-lightly.examples.whispers.chinese-whispers
  (:require [thornydev.go-lightly :as go]))

(defn whisper [left right]
  (go/put left (inc (go/take right))))

(defn whispers-as-you-go
  "build from right to left - construct the go routines as you need
   them and let the threads finish to be reused by other go routines"
  [nthreads]
  (let [n (Integer/valueOf nthreads)
        rightmost (go/channel)
        _         (go/go (go/put rightmost 1))
        leftmost (loop [right rightmost i (dec n)]
                   (let [left (go/channel)]
                     (go/go& (whisper left right))
                     (if (zero? i)
                       left
                       (recur left (dec i)))))]
    (println (go/take leftmost)))
  (go/stop))

The results are much more satisfactory:

user=> (time (w/whispers-as-you-go 10000))
10001
"Elapsed time: 886.232145 msecs"
nil
user=> (time (w/whispers-as-you-go 100000))
100001
"Elapsed time: 9276.837778 msecs"
nil

It can finish now, but it is 5 orders of magnitude slower than the Go version.

This program can have wildly different performance outcomes on different platforms. Yi Wang documents examples of this in a blog post where finds that an implementation in Racket is 42 times slower than the Go version, but writing an implementation in Haskell, which has a lightweight thread model, is comparable in performance to the Go version.

We should keep this JVM limitation in mind when constructing our apps.


Back to main wiki page