TechForce: Goroutines and Channels

I think most programmers are familiar with Process and Thread. But Go brings us a something new: “Goroutines”.

A Process is like our company which has three “children” companies in Japan, China and Austria, while a Thread is like a team within our company.

A Goroutine, in this analogy, is like a developer. It is less expensive and less complicated to add a new one than add a new process or thread, it shares Process resource (company resources), and can run in different threads (work in different teams).

Adding a new process (opening a new company) is expensive. Adding a new thread (team) is cheaper than a process, but still it needs some resources, while Goroutines are much smaller and cheaper to add than either processes or threads.

Creating a Goroutine

Creating a Goroutine is super easy, you just need to add the keyword “go” in front of the function name, or you can create an anonymous Goroutine.

CODE:

<code>package main

import (
     "fmt"
     "time"
)

func main() {
     go Task1()

     go func() {
          fmt.Println("Anonymous Task done!")
     }()

     time.Sleep(1000 * time.Millisecond)
}

func Task1() {
     fmt.Println("Task1 done!")
}
</code>

OUTPUT:

Use of Channels to communicate between Goroutines.

Channels combine communication – the exchange of a value – with synchronisation, guaranteeing that two calculations (Goroutines) are in a known state (from the Golang official site).

Consider the following scenario:

  1. A’s duty is to pick apples
  2. One basket can only hold one apple at a time.
  3. B eats apples
  4. A can only put an apple in the basket when the basket is empty, and B can only take an apple when there is an apple in the basket.

Without a channel, there will be problems:

CODE:

<code>package main

import (
     "fmt"
     "time"
)

func main() {
     go A()
     go B()
     time.Sleep(100 * time.Second)
}

func A() {
     for {
          time.Sleep(1 * time.Second)
          fmt.Printf("A: Pick an apple \n")
     }
}

func B() {
     for {
          time.Sleep(2 * time.Second)
          fmt.Printf("B: Eat an apple \n")
     }
}
</code>

OUTPUT:

A channel will solve this problem:

CODE:

<code>package main

import (
     "fmt"
     "time"
)

var basket chan int

func main() {
     //Create a channel  
     basket = make(chan int) 
     go A()
     go B()
     time.Sleep(100 * time.Second)
}

func A() {
     for {
          time.Sleep(1 * time.Second)
          // A only can put apple in basket when the basket is empty, otherwise the operation will be blocked
          basket <- 1
          fmt.Printf("A: Pick an apple \n")
     }
}

func B() {
     for {
          // a basket can only have one apple, when there is an apple in the basket B can take one, otherwise the operation will be blocked
          <-basket
          time.Sleep(2 * time.Second)
          fmt.Printf("B: Eats an apple \n")
     }
}
</code>

OUTPUT:

Synchronization Problem

Be careful when different Goroutines operate with the same resources. Like this one:

CODE:

<code>package main

import (
     "fmt"
     "time"
)

var name string

func main() {
     go MyNameIsA()
     go MyNameIsB()
     time.Sleep(3 * time.Second)
}

func MyNameIsA() {
     name = "A"
     time.Sleep(2 * time.Second)
     fmt.Printf("A said my name is %s \n", name)
}

func MyNameIsB() {
     name = "B"
     time.Sleep(1 * time.Second)
     fmt.Printf("B said my name is %s \n", name)
}
</code>

OUTPUT:

The result is not what we expected. It is due to the GoroutineMyNameIsA and the Goroutine MyNameIsB writing/reading the variable “name” at the same time and the variable “name” not being locked when one of them is in use. In this situation, we can use the package “sync”.

CODE:

<code>package main

import (
     "fmt"
     "sync"
     "time"
)

var name string
var m *sync.Mutex //create a mutual exclusion lock.

func main() {
     m = new(sync.Mutex)
     go MyNameIsA()
     go MyNameIsB()
     time.Sleep(5 * time.Second)
}

func MyNameIsA() {
     //if the lock is already in use, the goroutine called will be blocked until the mutex is available (when the mutex is unlocked).
     //So, MyNameIsB can be run until mutex be unlock
     m.Lock()
     name = "A"
     time.Sleep(2 * time.Second)
     fmt.Printf("A said my name is %s \n", name)
     m.Unlock()
}

func MyNameIsB() {
     m.Lock()
     name = "B"
     time.Sleep(1 * time.Second)
     fmt.Printf("B said my name is %s \n", name)
     m.Unlock()
}
</code>

OUTPUT:

Goroutines count and Thread count

Goroutines will run in the current thread. If there is no thread available (because it is blocked or another issue exists), it will create a new thread.

CODE:

<code>package main

import (
     "time"
)

func main() {
     for i := 0; i < 10; i++ {
          go GoroutineWithoutBlock()
     }
     time.Sleep(100 * time.Second)
}

func GoroutineWithoutBlock() {
     i := 1
     for {
          i++
     }
}
</code>

Since Goroutines are neither sys calls nor I/O operations, creating even ten Goroutines will create only two threads.

CODE:

<code>package main

import (
     "fmt"
     "time"
)

func main() {
     for i := 0; i < 10; i++ {
          go GoroutineWithoutBlock()
     }
     time.Sleep(100 * time.Second)
}

func GoroutineWithoutBlock() {
     for {
          fmt.Println("call system call, thread be blocked")
     }
}
</code>

In this situation, each thread is blocked by a sys call, so each Goroutine will create a new thread.


TechForce is a weekly meeting held at The Plant for developers to present and discuss new technologies and projects they are working on. Each week a different developer presents.