Concurrency in Go

Go is described as a concurrent -friendly language. The reason behind this is that it provides a simple syntax over two powerful mechanisms – goroutines and channels. Before we go forward with understanding the use of Goroutines and Channels in Go and how to implement them, let me make it clear that concurrency does not imply parallelism. Concurrency is about dealing with multiple task at once! Whereas parallelism is about doing  multiple task simultaneously. Concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.

Goroutines:                                                                                                                     

A Goroutine is similar to a thread but it is scheduled by Go and not the Os. Code that runs in a Goroutine can run concurrently with other code. Goroutines are easy to create and have little overhead. Most of the goroutines will end up running on the same underlying Os thread. Hence Goroutines are means of creating concurrent architecture of a program which could possibly execute in parallel in case the hardware allows it. Note that Goroutines run in the same address space, so access to shared memory must be synchronized.Lets take a look at an example.


package main

import (
     “fmt”
     “time”
)

func say(s string) {
     for i := 0; i < 5; i++ {
         time.Sleep(10* time.Millisecond)
         fmt.Println(s)
       }
}
func say1(s string) {
     for i := 0; i < 5; i++ {
          time.Sleep(10* time.Millisecond)
          fmt.Println(s)
      }
}

func main() {
go say(“world”)
say1(“hello”)
}


go say(“world”)  starts a new go gubroutine whereas say1(“hello”) is executed in the current goroutine. The output of above code is as follows:


hello world                                                                                                                                                                                    hello world                                                                                                                                                                                    hello world                                                                                                                                                                                    hello world                                                                                                                                                                             hello world


Creating goroutines is trivial, and they are so cheap that we can start many. However, concurrent code needs to be coordinated. Let us see why!


package main
import (
    “fmt”
    “time”
)
var counter = 5
func main() {
    for i := 0; i < 2; i++ {
        go increment()
   }
    time.Sleep(time.Millisecond * 10)
}
func increment() {
    counter++
    fmt.Println(counter)            
}

What do you think will be the output?  6 and 7 ?. When you run the above code, that is probably what you might get but in actual the behavior undefined. This is because, we have to goroutines trying to write to the same variable -counter simultaneously or in worst case, while one is writing to it, the other must be reading it. Could this possibly be dangerous? Yes, absolutely. counter++ might seem like a simple line of code, but it actually gets broken
down into multiple assembly statements – the exact nature is dependent on the platform that you’re running. The system may even crash due to this.
Hence we need to synchronize the writes. There are various ways to do this, including using some truly atomic operations that rely on special CPU instructions. The most common approach is to use a mutex. However if we don’t use mutex wisely, we may end up with deadlocks.

Channels to the rescue!                                                   

A channel is a communication pipe between goroutines which is used to pass data. In other words, a goroutine that has data can pass it to
another goroutine via a channel. The result is that, at any point in time, only one goroutine has access to the data. Channels serve to synchronize execution of concurrently running functions and to provide a mechanism for their communication by passing a value of a specified type. Channels have several characteristics: the type of element you can send through a channel, capacity (or buffer size) and direction of communication specified by a <- operator. You can allocate a channel using the built-in function make:


c := make (chan int)


Channel supports two operations:                                                                                                                                               a) Sending   CHANNEL

We can think of a channel as a conveyer belt or a pipe having defined size and capacity.The number of items that the channel (which is our conveyer belt) can hold is important. It indicates how many items can be worked with at a time. Even if the sender is capable of producing multiple items, if the receiver is not capable of accepting them, then it won’t work.   If the capacity of the channel is 1 – i.e. once an item is placed on the channel, it has to be taken off before another one is put in its place, this becomes a synchronous channel. Each side – the sender and the receiver – is communicating one item at a time, and has to wait until the other side performs either a sending or a receiving correspondingly. If the channel capacity is not specified ,by default, senders and receivers block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.  If a channel is unbuffered , the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value has been copied to buffer. If the buffer is full, sender will wait until some receiver has received the values. With this understanding, let us look at some codes.


package main

import (
     “fmt”
     “time”
     “strconv”
)

var i int

func makeItem(cs chan string) {
     i = i + 1
    itemName := “Item ” + strconv.Itoa(i)
    fmt.Println(“Making an Item and sending …”, itemName)
    cs
}

func receiveAndPackItem(cs chan string) {
     s :=
     fmt.Println(“Packing received item: “, cs)
}

func main() {
     cs := make(chan string)
     for i := 0; i<3; i++ {
        go makeItem(cs)
        go receiveAndPackItem(cs)

       //sleep for a while so that the program doesn’t exit immediately and output is    //clear for illustration
      time.Sleep(1 * 1e9)
     }
}


Output is as follows:

Making an item and sending … Item 1
Packing received item: Item 1
Making an item and sending … Item 2
Packing received item: Item 2
Making an item and sending … Item 3
Packing received item: Item 3
So what happens when the above program is executed? As we can see in the for loop, each time two new goroutines are created to execute makeItem and receiveAndPackItem and both the goroutines listen to the same channel. makeItem creates an item and sends it on the channel and the receiver receives it from the channel and packs it. Since the channel is synchronous with a default
capacity 1, making an item and packing it should happen before a new item is made and sent to the channel. Now let’s play around with the code to understand it in depth.

package main
import (
    “fmt”
    “time”
    “strconv”
)

func makeItem(cs chan string, count int) {
     for i := 1; i <= count; i++ {
         itemName := “Item ” + strconv.Itoa(i)                                                           

        fmt.Println(“Making Item:”, itemName)
        cs
      }
}

func receiveAndPackItem(cs chan string) {
     for s := range cs {
        fmt.Println(“Packing received item: “, s)
     }
}

func main() {
     cs := make(chan string)
     go makeItem(cs, 5)
     go receiveAndPackItem(cs)

//sleep for a while so that the program doesn’t exit immediately
    time.Sleep(3 * 1e9)
}


When we run the above code, the receiverAndPackItem runs an infinite forloop. It does not know how many items are going to be pushed to the channel and when it should stop listening to the channel. A sender can close a channel to indicate that no more values will be sent. Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression:         v, ok := <-c

if ok is false, there are no more values to be sent and the channel is closed.  Go provides the range keyword which when used with a channel will wait on the channel until it is closed.  The output of the above code is:

Making Item: Item 1
Packing received item: Item 1
Making Item: Item 2
Making Item: Item 3
Packing received item: Item 2
Packing received item: Item 3
Making Item: Item 4
Making Item: Item 5
Packing received item: Item 4
Packing received item: Item 5

It is important that we understand that the output as shown is not the correct reflection of the actual sending and receiving on the channel. The sending and receiving here is synchronous – one item is made at a time and packed immediately. However due to the time lag between the print statement and the actual channel sending and receiving, the output seems to indicate an incorrect order. So in reality what is happening is:

Making Item: Item 1
Packing received item: Item 1                                                                                                                                                     Making Item: Item 2
Packing received item: Item 2                                                                                                                                                    Making Item: Item 3
Packing received item: Item 3                                                                                                                                                    Making Item: Item 4
Packing received item: Item 4                                                                                                                                                    Making Item: Item 5
Packing received item: Item 5

Select:

The select keyword when used in conjunction with many channels checks which channel is ready and accordingly performs transmission or reception of data to/from that channel. The case blocks within it can be for sending or receiving – when either a send or a receive is initiated with the <- operator, then that channel is ready. There can also be a default case block, which is always ready. The algorithm with which the select keyword works on blocks can be approximated to this:
* check each of the case blocks
* if any one of them is sending or receiving, execute the code block corresponding to it
* if more than one is sending or receiving, randomly pick any of them and execute its code block
* if none of them are ready, wait
* if there is a default block, and none of the other case blocks are ready, then execute the default block (I’m not very sure about this, but from coding experiments, it appears that default gets last priority).

Now lets take a look at this final code.


package main

import (
     “fmt”
     “strconv”
     “time”
)

func makeBeverage(cs chan string, flavor string, count int) {
     for i := 1; i <= count; i++ {
        beverageName := flavor + strconv.Itoa(i)
        cs
     }
     close(cs)
}

func receiveAndPackBeverage(coffee_cs chan string, tea_cs chan string) {
     coffee_closed, tea_closed := false, false

     for {
         //if both channels are closed then we can stop
         if coffee_closed && tea_closed {
             return
         }
        fmt.Println(“Waiting for a new beverage …”)
       select {
           case beverageName, coffee_ok := <-coffee_cs:
            if !coffee_ok {
           coffee_closed = true
           fmt.Println(” … Coffee channel closed!”)
          } else {
          fmt.Println(“Received from Coffee channel. Now packing”, beverageName)       }                                                                  

case beverageName, tea_ok := <-tea_cs:
      if !tea_ok {
      tea_closed = true
      fmt.Println(” … Tea channel closed!”)
      } else {
      fmt.Println(“Received from Tea channel. Now packing”, beverageName)
}
   }
  }
}

func main() {
     coffee_cs := make(chan string)
     tea_cs := make(chan string)
     go makeBeverage(tea_cs, “tea”, 3)
     go makeBeverage(coffee_cs, “coffee”, 3)

    go receiveAndPackBeverage(coffee_cs, tea_cs)

    //sleep for a while so that the program doesn’t exit immediately
    time.Sleep(2 * 1e9)
}


The output is as follows:

Waiting for a new beverage …
Received from Tea channel. Now packing tea1
Waiting for a new beverage …
Received from Coffee channel. Now packing coffee1
Waiting for a new beverage …
Received from Tea channel. Now packing tea2
Waiting for a new beverage …
Received from Coffee channel. Now packing coffee2
Waiting for a new beverage …
Received from Tea channel. Now packing tea3
Waiting for a new beverage …
Received from Coffee channel. Now packing coffee3
Waiting for a new beverage …
… Coffee channel closed!
Waiting for a new beverage …
… Tea channel closed!

This output actually makes sense with respect to the above algorithm. Whenever the tea/coffee channel is empty, it accepts another tea/coffee from the sender and when a tea or a coffee is send to a tea/coffee channel, it becomes ready to output the data to a receiver. If there is no more tea/coffee to be sent to the tea/coffee channel, coffee_ok/tea_ok becomes false and the receiveAndPackBeverage will stop listening to the respective channel.

Resources: The Little Go Book , A Tour of Go

                                                     THE END

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s