Wow, the Go Memory Model really threw me

So 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.

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

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/* Compile with `gcc mem.c -lpthread` */

#include

#include

intdone=0;

void*Toggle(void*t){

done=1;

pthread_exit(NULL);

}

intmain(intargc,char*argv[]){

pthread_tt;

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

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

/*

* Build: javac mem.java

* Run: java mem

*/

classDone{

publicstaticbooleandone;

}

classMyThreadextendsThread{

publicvoidrun(){

Done.done=true;

}

}

classmem{

publicstaticvoidmain(String[]args){

MyThreadt=newMyThread();

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

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// To run: node mem.js

vardone=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.

Python

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

#!/usr/bin/python

importthread

done=False

deftoggle(dummy):

globaldone

done=True

thread.start_new_thread(toggle,("",))

whilenotdone:

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.

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).

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.

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.

Philip O'Toole

Summary

My name is Philip O'Toole and I am an experienced software engineer from Ireland. Based in the Greater Pittsburgh area, I have a particular interest in all things related to software development, particularly Linux system software, databases, distributed systems, and SaaS platforms.