A Programmer's Guide To Go Part 3 - Goroutines And Concurrency
Written by Mike James   
Thursday, 09 January 2014
Article Index
A Programmer's Guide To Go Part 3 - Goroutines And Concurrency
Channels

More than one thread

In many cases Go's default 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. 

For example if you add GOMAXPROCS(2) to the start of the previous 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.

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 you will sometimes see:

0
1
end
2

and sometimes

0
end
1
2

depending on how fast the thread running the goroutine is. 

Channels

So far the facilities for concurrent programming look a little ad-hoc if easy to use. The whole thing suddenly starts to make sense when you learn about channels. A channel is a simple data structure - something like a typed array or buffer - that can be shared safely between goroutines. 

To declare a channel you use something like:

ch:=make(chan type,size)

where type is the type of data stored int he channel and size is the number of elements. If you don't specify a size you get a single element or unbuffered channel. 

To assign a value to a channel you use the <- operator and to retrieve a value you use the -> operator. For example:

ch <- a

and

a <- ch

This is where things get interesting. As well as allowing values to be passed channels also act as a blocking mechanism that frees a thread to run another goroutine. For a single element channel the rules are:

a routine that performs a channel read blocks until a value is available.

a routine that performs a channel write blocks until the value has been read by another routine

These two rules result in the more or less automatic passing of the thread of execution between goroutines - you don't need to worry about calling Sleep or another blocking operation simply reading and writing to a channel frees the thread to run another goroutine. 

To see this in action consider the following main program:

func main() {
    ch := make(chan int)
    go count(ch)
    j := <- ch
}

This creates an unbuffered int channel calls a goroutine, passing it the channel and then retrieves a value from the channel. When the main program reached the channel read it blocks because the goroutine is also blocked and there is no value in the channel. The thread is freed up and runs the goroutine which could be something like:

func count(ch chan int) {
    ch <- -1
}

This stores -1 in the channel and blocks so freeing the thread of execution which returns to the the main program. This retrieves the value from the channel and ends along with the goroutine.

You can see that the channel in this case both allows communication between the two routines i.e. the passing of the value -1 and the automatic sharing of the thread of execution. That is when the main program needed a value from the goroutine it blocked which allowed the goroutine to execute until it had the value that the main program needed when it blocked and the main program started. All very neat!

For a slightly more advanced example consider:

func main() {
    ch := make(chan int)
    go count(ch)
    for {
        j := <-ch
        if j < 0 {
          break
        }
        fmt.Println(j)
    }
}

This calls the goroutine count and then blocks waiting for values that it returns until it gets a -1 which it treats as a "terminate" condition.  The goroutine is:

func count(ch chan int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    ch <- -1
}

Notice that the for loop in the goroutine blocks and is effectively suspended until the main program has finished processing the value and asks for another. This is more subtle than you might think at first because while the goroutine unblocks when the main routine reads the value from the channel it doesn't get the thread back until the main routine asks for another value and blocks. Consider how this would be different if you allocated two threads to run program. 

Go's channel based concurrency is subtle but mostly safe and easy to use. The trick is that for a goroutine to run it has to be unblocked and there has to be a thread free to run it.

Buffered Channels

Now we have to look at buffered channels. In the case of a buffered channel the blocking rules are that the routine that reads from the channel blocks until there is something to read. The routine that writes only blocks only if the channel is full and a value cannot be written.

Buffered channels basically allow situations where a goroutine can fill the buffer before giving up the thread to other routines to empty the buffer. It can also be used to implement classic concurrency object such as the semaphore. You can use the size of the buffer to limit the number of routines accessing a resource.

It is difficult to find a very simple example of using buffered channels but this one is as close to simple as I can manage. Consider the following main program:

 

func main() {
    ch := make(chan int, 2)
    go count(ch)
    for {
        j := <-ch
        if j < 0 {
            break
        }
        fmt.Println("main", j)
    }
}

 

This simply sets up a channel with two elements and then reads values from it printing them as it goes - again -1 is taken as a signal to stop.

The goroutine is just:

func count(ch chan int) {
    for i := 0; i < 10; i++ {
        ch <- i
        fmt.Println("goroutine", i)
    }
    ch <- -1
}

This adds values to the channel and then prints the value.

What you will see is:

goroutine 0
goroutine 1
main 0
main 1
goroutine 2
goroutine 3
main 2
main 3

and so on.

What happens is that the goroutine gets the thread when the main routine asks for a value and blocks. It places two values in the channel and prints them out. When it tries to add a third value it blocks because the channel is full and so the thread returns to the main routing which prints two value and then blocks because the channel is empty. The goroutine then adds two more values and blocks and so on until its all done. 

Try changing the size of the channel to 3 and you will see each routine print three values each time. 

Consider how all of this changes if there is more than one thread of execution.

Where Next

In these three brief looks at Go we have examined the key features that make it special - types, objects and concurrency. Go is still a very young language and it has lots of room to expand, but currently you have to admit that it is light and flexible. It is a language to keep an eye on for the future. 


goicon1

A Programmer's Guide To Go

  1. A Programmer's Guide To Go With LiteIDE
  2. A Programmer's Guide To Go Part 2 - Objects And Interfaces
  3. A Programmer's Guide To Go Part 3 - Goroutines And Concurrency

More Information:

http://golang.org

Related articles:

Go Programming Language Turns 3       

Getting started with Google's Go

Why invent a new language? Go creator explains

Ready to Go - Go Reaches Version 1

Go in Google App Engine

Google App Engine Go-es Forward

Go with Google - Yet Another Language!

A Programmer's Guide to Scratch 2

Getting Started With NetLogo

Getting Started With TypeScript

A Programmer's Guide To Octave

Getting Started With Google App Script

 

Banner

 

To be informed about new articles on I Programmer, install the I Programmer Toolbar, subscribe to the RSS feed, follow us on, Twitter, FacebookGoogle+ or Linkedin,  or sign up for our weekly newsletter.

blog comments powered by Disqus

<ASIN:0321817141>

<ASIN:1478355824>



Last Updated ( Friday, 10 January 2014 )
 
 

   
RSS feed of all content
I Programmer - full contents
Copyright © 2014 i-programmer.info. All Rights Reserved.
Joomla! is Free Software released under the GNU/GPL License.