Wow, the Go Memory Model really threw me

gopherSo far coding in Go has been fun. It comes with nice functionality that lets you know that the Go team really have been writing system software (useful stuff like this, and this). And then I read about the Go Memory Model, and had my consciousness raised.


I was startled when the page stated that this program may never exit:
// To build: go build mem.go
package main
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}


Yes, it never exits. I checked. On 64-bit Kubuntu 14.04. This really threw me because, at a cursory glance, Go code looks a lot like C. I mean, there’s the global at the top. It’s in scope. And in the goroutine the variable done really does refer to the global because if done is replaced with some other undeclared variable, the program won’t compile.

So this got me thinking. What about the other languages I work with? Have I been taking behaviour for granted? Let’s take a look.

C

/* Compile with `gcc mem.c -lpthread` */
#include
#include
int done = 0;
void *Toggle(void *t) {
done = 1;
pthread_exit(NULL);
}
int main(int argc, char *argv[]) {
pthread_t t;
pthread_create(&t, NULL, Toggle, (void *)t);
while (!done) {
}
printf("Flag toggled!n");
pthread_exit(NULL);
}


This behaves exactly as I expected. It exits immediately, printing Flag toggled! Of course, this didn’t surprise me since I have been writing C code for a long time, C gives you direct access to the process’ memory space, but it’s always clearer when looking at two similar-looking pieces of code side-by-side that behave differently. Next up, Java.

Java

/*
* Build: javac mem.java
* Run: java mem
*/
class Done {
public static boolean done;
}
class MyThread extends Thread {
public void run() {
Done.done = true;
}
}
class mem {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
while (!Done.done) {
}
System.out.println("Flag toggled!");
}
}


Same result, immediate exit. I would have guessed as much, but Java does run inside its own VM, so perhaps I had missed something all these years. Let’s try something very different — node.js.

node.js

// To run: node mem.js
var done = false;
setTimeout(function() {
done = true;
}, 1000);
setInterval(function() {
if (!done) {
return;
}
console.log("Flag toggled!");
process.exit();
}, 1000);


Due to the asynchronous nature of node.js, this program takes a second or two to exit. But it exits — again, no surprise to me.
Finally, let’s check out Python.

Python

#!/usr/bin/python
import thread
done = False
def toggle(dummy):
global done
done = True
thread.start_new_thread(toggle, ("",))
while not done:
continue
print 'Flag toggled!'


Python is a little more interesting because without the global keyword the program does not exit. done is local to its function and different to the global done, giving the impression that it works the same way as Go. But include global and the program does exit immediately.

Lessons Learned

Take the time to study a new language. Go has some really nice documentation, and I’m glad I took the time to read it. While I’d never write code like this for a production system, the patterns shown above are very common when one is hacking something up, or performing some quick debugging. All the other languages behaved as I expected, but if one didn’t know better, this behaviour of Go could be very confusing. The code snippet looks very similar to the other examples, but works very differently.

GOMAXPROCS

The golang-nuts mailing enlightened me on why this happens. Since GOMAXPROCS defaults to 1, the for loop never yields the processor, so the goroutine never runs. It’s exactly the reason the node.js program does exit — because it has been written such that the code that is looping explicitly yields the processor every loop. If the node.js program’s loop never yielded the processor, it would behave the same way as the Go program on my machine. It this sense, when GOMAXPROCS is 1, node.js and Go are similar programming environments.

As the Memory Model guide states at the end …the solution is the same: use explicit synchronization.

5 thoughts on “Wow, the Go Memory Model really threw me”

  1. // Try this code will work on multi core CPU, otherwise as you described
    // and only sync helps
    package main
    import “runtime”
    var a string
    var done bool
    func setup() {
    a = “hello, worldn”
    done = true
    }
    func main() {
    runtime.GOMAXPROCS(2)
    go setup()
    for !done {
    }
    print(a)
    }

  2. Just so this doesn’t get taken as fact by someone googling:
    1) Your C-program exhibits the same issue when using an optimizing compiler – the “done” variable is read and evaluated once and the program ends in an infinite loop (unless the new thread writes to “done” before that).
    2) The java example has the same issue. See:
    http://stackoverflow.com/a/8432655/681490
    3) node.js isn’t multithreaded (the timeout executes in the same thread as the settimeout), so the comparison is useless.
    I don’t know Python. Maybe it has memory barriers for all access to global variables (which is slow, but safer). Also, if it isn’t optimizing, it may very well “trickle down” at some point.
    Either way, you always need to use a synchronized method of transferring data, which is why it is great to have a channel that does exactly what you need.

  3. Klaus – thanks for your feedback.
    As for optimization of the C code, I deliberately compiled without optimization, for the reasons you stated. I’ve also written a fair amount of node.js, and fully realize that it is single-threaded. That was what I was pointing out towards the end. Perhaps I should have been clearer.
    As I noted towards the end of the post, this was never meant to demonstrate production code. It was deliberately written in a naive fashion, to show the interesting difference between a simple Go (which is very new to me) program and an equally simple and naive, say, C program.
    Of course, there are many ways to code these programs, some of which wouldn’t behave as they in these simple examples.

  4. Your Java implementation may never return either, since there is no memory barrier between the read and write. Chances are it should return eventually, but exact behavior would depend on many factors, like version and OS-specific JVM and compiler implementation details.

Leave a Reply to Vlad Cancel reply

Your email address will not be published. Required fields are marked *