A Programmer's Guide To Go Part 3 - Goroutines & Concurrency
Written by Mike James   
Thursday, 13 February 2020
Article Index
A Programmer's Guide To Go Part 3 - Goroutines & Concurrency
Blocking Operations
Channels
Buffered Channels

Blocking operations

So how do we suspend the main function so that the goroutine gets a chance to run on the same single thread?

In the real world blocking operations generally involve I/O of some kind, be it to disk or via a network connection. Go's scheduler has grown more sophisticated over time and now has an opertunity to schedule another goroutine when ever a function is called or a system call is made. This means that it can be difficult to know when a thread will switch its attention from one goroutine to another. Unlike asynchronous languages such as JavaScript code that doesn't block to do I/O isn't guaranteed to run to the end before being interrupted. This can create synchronization problems and race conditions as demonstrated earlier. Go provides a special data type - the channel - that is often used to implement a blocking exchange of data between goroutines in a synchronized manner, but more of this a little later.

For the sake of an example, the simplest way of blocking the main program or any goroutine is to use the time module and the Sleep method.

If you call Sleep(t) the thread running the function is freed up for t milliseconds. In that time it can run other goroutines. Each goroutine gets to run until it blocks and the thread is used to run another goroutine or perhaps the main function.

To give the goroutine time to run we need to add a call to the Sleep method of the time object.

For example:

import (
    "fmt"
    "time"
)
func main() {
    runtime.GOMAXPROCS(1)
    go count()
    time.Sleep(1)
    fmt.Println("end")
}

func count() { 
   for i := 0; i < 1000; i++ {
        fmt.Println(i)
    }
}

If you run this program you will see the goroutine print 0 to some value and then the main program print "end". The goroutine didn't get a chance to run until the call to Sleep. Once it got the thread of execution the goroutine then ran until the main goroutine's sleep ended and it printed "end".

Now consider the following program and try and work out what you expect it to print:

import (
    "fmt"
    "time"
)
func main() {
   runtime.GOMAXPROCS(1)
    go count()
    time.Sleep(1)
    fmt.Println("end")
}

func count() { 
   for i := 0; i < 1000; i++ {
        fmt.Println(i)
        time.Sleep(1)

    }
}

Notice that the only change it that now the goroutine has a call to Sleep in it. 

What you see when you run the program is something like;

0
1
end

The reason should be obvious. The main program blocks for 1 millisecond and the goroutine gets to run. The goroutine prints 0 and then blocks by executing Sleep(1). When the main goroutine wakes up it calls Print;n which is a system call and if count has finished sleeping it might get another chance to print a value, so transfering the thread back to the main goroutine which ends the program.

You can try modifying the sleep times. If you make count sleep for long enough 300ms on the machine I used then you will only see 0 printed as the program ends before it has time to be restared.

Notice that none of these examples is to be taken as a suggestion that a real Go program that uses concurrency should perform such timing-based synchronisation. The purpose of the examples is to understand how Go works with threads to keep things moving. This is another program that has a race condition such that what you see depends on how long things take.

It is also important to realize that goroutines all share the same memory and in particular the same globals. If a goroutine is invoked as a function literal then it not only shares the globals but because it is a closure it shares the local variables of the function it is defined in. This is a subtle point but worth remembering. 

goruns

More than one thread

In many cases one thread concurrency is all you need to create concurrent programs that remain responsive to the outside world. After all many a modern programming language - JavaScript, Python and so on manage with a single thread or single UI thread.

If you do want to work with more than one thread then there are a number of ways of doing so but the simplest is to use the GOMAXPROCS method of the runtime. Notice that the default is to have as many threads are there are cores on the machine that is being used.

For example if you add GOMAXPROCS(2) to the start of an earlier example: 

import (    
    "fmt"
    "runtime"
    "time")

func main() {
    runtime.GOMAXPROCS(2)
    go count()
    time.Sleep(1)
    fmt.Println("end")
    for {
    } 
}

func count() {
    for i := 0; i < 1000; i++ {
        fmt.Println(i)
        time.Sleep(1)
    }
}

 

Notice that now the main program ends with an infinite loop.

What would you expect to happen in the single thread case?  

The main program would release the thread to the goroutine which would print zero and then block, so releasing the thread back to the main program which then enters an infinite loop and keeps the thread busy.

This is not what happens with two threads.

As we have allocated two threads to the program the go routine starts to run as soon as it is called on a separate thread to the main program. It prints zero and then blocks for 1 millisecond. The main program continues to run on its thread and also blocks for 1 millisecond. When the main program restarts it prints "end" and starts an infinite loop. However in this case the goroutine isn't blocked because it is running on a different thread. When its sleep is over it continues to print the rest of the values up to 999.

This is a true multithreaded program and while it can be more powerful it can be more difficult.

In particular if you are using multiple threads your programs are no longer deterministic because the order that things occur in depends on how the threads are scheduled. 

For example if you run the demo program repeatedly with a delay of only 1 millisecond you will sometimes see:

0
1
end
2

and sometimes

0
end
1
2

depending on how fast the thread running the count goroutine is. 



Last Updated ( Friday, 14 February 2020 )