LearnGoroutines Make Concurrency (Almost) Easy

Photo via Wikipedia
   
Treehouse

Treehouse
writes on July 24, 2017

A program that supports concurrency can carry out several operations at the same time. That’s especially important on today’s multi-core computer processors. A program that uses 4 cores at once could theoretically run almost 4 times as fast (well, for certain operations). But programs without concurrency support can usually only use a single core, which lets a lot of processing power go to waste.

Enter Goroutines

When the creators of the Go programming language were writing up their wish list for a new language, they wanted to make it easy to write concurrent programs. Their solution comes in the form of 2 features: goroutines, which are functions that run concurrently, and channels, which simultaneously allow communication and synchronization between goroutines.

An example is probably the quickest way to explain… Suppose you have a generateKey function, which uses a super-secret algorithm to generate cryptographic keys. The function is a bit slow, averaging 3 seconds each time it’s called. So if a program calls generateKey 3 times in a row, that program will take just over 9 seconds to run.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func generateKey() int {
    fmt.Println("Generating key")
    // Super-secret algorithm!
    keys := []int{3, 5, 7, 11}
    key := keys[rand.Intn(len(keys))]
    // It's kinda slow!
    time.Sleep(3 * time.Second)
    fmt.Println("Done generating")
    return key
}

func main() {
    rand.Seed(time.Now().Unix())
    // Call generateKey 3 times.
    for i := 0; i < 3; i++ {
        fmt.Println(generateKey())
    }
    fmt.Println("All done!")
}

The above program produces this output:

Generating key
Done generating
5
Generating key
Done generating
7
Generating key
Done generating
11
All done!

…And as predicted, it takes about 9 seconds. But what if we used goroutines to run the 3 calls to generateKey concurrently? Theoretically, the whole program could run in just over 3 seconds!

A "main" goroutine, running concurrently with several other goroutines.

Goroutine Syntax

Using goroutines is really simple. In fact, you’re automatically using a single goroutine any time you run a Go program, because the main function always runs within a goroutine. Of course, no code will run concurrently unless you create additional goroutines. But that’s not hard to do: just put the keyword go before any function call. (Compare that to Java’s threads, where you have to create a whole new class!) Your main goroutine will immediately resume running after the function call, and the function you called will run simultaneously (that is, concurrently) alongside it, as another goroutine.

Let’s try this out. We’ll just add the go keyword before the call to generateKey:

func main() {
    rand.Seed(time.Now().Unix())
    for i := 0; i < 3; i++ {
        fmt.Println(go generateKey())
    }
    fmt.Println("All done!")
}

But if we try to compile this, we get an error:

syntax error: unexpected go, expecting expression

…Because we’re not allowed to use the go keyword when you’re using a return value from a function. And really, that makes sense. If the main function resumes running before generateKey returns, then what return value will we pass to fmt.Println?

So we’re not really sure how to get the generated keys back. But let’s not give up on using goroutines just yet. We’ll just remove the call to fmt.Println from around go generateKey():

func main() {
    rand.Seed(time.Now().Unix())
    for i := 0; i < 3; i++ {
        go generateKey()
    }
    fmt.Println("All done!")
}

If we try running again, here’s the output we get:

All done!
Generating key
Generating key

Okay… this is kind of a mess. Our main goroutine reaches its end, and prints “All done!”. While this is happening, 2 of our generateKey goroutines begin running, and print “Generating key”. The program exits before the third generateKey goroutine even gets a chance to print anything. (And this program will behave diffferently each time you run it; you can’t be sure when it will exit!)

So now we have 2 problems:

  1. We don’t have a way to get a value back from the other goroutines
  2. We don’t have a way to wait until the other goroutines finish before the main gorooutine exits

Channels to the Rescue

We can solve both of those problems at once with Go channels. A goroutine can write values to a channel, and other goroutines can read them back out. If no values have been written to the channel yet, the goroutine that’s attempting to read from it will wait until a value is added. So channels accomplish 2 things at once:

  1. Communication between goroutines
  2. Synchronization between goroutines

We can create a channel by calling the built-in function make with the type of channel we want to create. (That is, we need to specify what type of values our channel will hold.) myChannel := make(chan bool) makes a channel that holds boolean values, and assigns it to the myChannel variable. make(chan string) will make a channel for strings, make(chan int) makes a channel for integers, and so on.

Once we have a channel, we can write and read values with the <- operator. For example, myChannel <- myValue writes myValue to myChannel, and myValue := <-myChannel reads a value from myChannel and assigns it to myValue.

Using Channels in Our Program

Let’s try getting our key generator working again using channels. We can update generateKey to take a channel as an argument. Instead of returning the key, we can have generateKey write the key to the channel.

Back in the main function, instead of looking for return values from generateKey, we can read keys from the channel. Again, this accomplishes 2 things at once:

  1. Gets the keys we want
  2. Causes the program not to exit until the keys are ready

Let’s try it! Our updates are marked with comments in the code below.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

// Update this function to accept a channel parameter,
// and remove the return value.
func generateKey(channel chan int) {
    fmt.Println("Generating key")
    keys := []int{3, 5, 7, 11}
    key := keys[rand.Intn(len(keys))]
    time.Sleep(3 * time.Second)
    fmt.Println("Done generating")
    // Write the key to the channel instead of returning.
    channel <- key
}

func main() {
    rand.Seed(time.Now().Unix())
    // Create a channel.
    channel := make(chan int)
    // Create 3 more goroutines.
    for i := 0; i < 3; i++ {
        go generateKey(channel)
    }
    // Read and print keys from the channel.
    // This also causes the program to wait until 3
    // keys have been read.
    for i := 0; i < 3; i++ {
        fmt.Println(<-channel)
    }
    fmt.Println("All done!")
}

If we try running this, here’s the output:

Generating key
Generating key
Generating key
Done generating
Done generating
11
3
Done generating
11
All done!

We print 2 of the keys before we’re done generating the third, so it’s not perfectly sequential, but such is the nature of concurrent programming. The important part is, the channel allows us to retrieve the keys, and it causes the program to wait until we have them all before exiting. And because everything happens concurrently, the whole process takes just over 3 seconds!

The Moral of the Story

Goroutines and channels are a simple way to add concurrency to your programs. If your program includes any long-running operations, like waiting for network connections or big calculations, give goroutines a try. You may find it’s an easy way to finish your tasks faster!

P.S.: I’ve made the code from this post available in a Treehouse Workspaces snapshot. You can try running the code right from your browser by forking it and typing go run generate_goroutines.go in the workspace console.

P.P.S: This post was adapted from Go Language Overview. It’s our new course for developers already proficient in programming, who want a quick primer on Go. We’ll be releasing courses for beginning Go programmers too, so keep an eye on our library!

Start learning to code today with a free trial on Treehouse.

GET STARTED NOW

Learning with Treehouse for only 30 minutes a day can teach you the skills needed to land the job that you've been dreaming about.

Get Started

Leave a Reply

You must be logged in to post a comment.

man working on his laptop

Are you ready to start learning?

Learning with Treehouse for only 30 minutes a day can teach you the skills needed to land the job that you've been dreaming about.

Start a Free Trial
woman working on her laptop