Channels

Do not communicate by sharing memory , share memory by communicating.

To be more useful, goroutines have to communicate: sending and receiving information between them and coordinating / synchronizing their efforts.

Communication forces coordination.

Goroutines could communicate by using shared variables, but this is highly discouraged because this way of working introduces all the difficulties with shared memory in multi-threading.

Channel is a special type in Go, it is like a conduit (pipe) through which you can send typed values and which takes care of communication between goroutines, avoiding all the pitfalls of shared memory; the very act of communication through a channel guarantees synchronization. Data are passed around on channels: only one goroutine has access to a data item at any given time: so data races cannot occur, by design. The ownership of the data (that is the ability to read and write it) is passed around.

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:

a := make(chan int)             // by default the capacity is 0 ,
                                // same as a := make(chan int,0)
                                // we called it an unbuffered channel.

b := make(chan string, 5)       // non-zero capacity
                                // we called it a buffered channel.

Communication operator <-

c <− 1          ← Send the integer 1 to the channel c
<− c            ← Receive an integer from the channel c
b := <− c       ← Receive from the channel c and store it in b

r := make(<-chan bool)          // can only read from
w := make(chan<- []os.FileInfo) // can only write to

Channels are first-class values and can be used anywhere like other values: as struct elements, function arguments, function returning values and even like a type for another channel:

// a channel which:
//  - you can only write to
//  - holds another channel as its value
c := make(chan<- chan bool)

// function accepts a channel as a parameter
func readFromChannel(a <-chan string) {...}

// function returns a channel
func getChannel() chan bool {
     b := make(chan bool)
     return b
}

Examples :

1. Deadlock

This simple code will fail,

package main

func main() {
     c := make(chan int,0)      // This is an unbuffered channel , same as c := make(chan int)
     c <- 42                    // write to a channel (deadlock create here!)
     println(<-c)               // read from a channel & print
}

[output]
fatal error: all goroutines are asleep - deadlock!
...

It is easy to create a deadlock in golang.

Why this example create deadlock ?

This error occurs because of the blocking nature of communication operations. The code here runs within a single thread, line by line, successively, the operation of writing to the channel (c <- 42) blocks the execution of the whole program , because writing operations on a synchronous channel can only succeed in case there is a receiver ready to get this data , but the receiver are in the next line.

By default, communication is synchronous and unbuffered:

Sends do not complete until there is a receiver to accept the value. One can think of an unbuffered channel as if there is no space in the channel for data: there must be a receiver ready to receive data from the channel and then the sender can hand it over directly to the receiver. So channel send/receive operations block until the other side is ready:

  1. A send operation on a channel (and the goroutine or function that contains it) blocks until a receiver is available for the same channel: if there’s no recipient for the value on channel, no other value can be put in the channel: no new value can be sent in when the channel is not empty. So the send operation will wait until the channel becomes available again: this is the case when the channel-value is received (can be put in a variable).
  2. A receive operation for a channel blocks (and the goroutine or function that contains it) until a sender is available for the same channel: if there is no value in the channel, the receiver blocks.

Although this seems a severe restriction, it works well in most practical situations.

To make this code work , we can do it this way:

package main

func main() {
     c := make(chan int,0)      
     go func() { c <- 42 }()    // Make the writing operation be performed in another goroutine.
     println(<-c)
}

[output]
42

or

package main

func main() {
     c := make(chan int,1)      // make a buffered channel
     c <- 42                    // write to a channel
     println(<-c)               // read from a channel & print
}

[output]
42

2. Synchronous (blocking) channels

a) blocking.go

package main

func main() {
     // Create a unbuffered channel (capacity = 0) to synchronize goroutines
     done := make(chan bool)

     go func() {
          println("-- goroutine --")
          done <- true
     }()

     println("-- main()    --")
     <-done         // main function run faster than goroutine , 
                    // so it wait here for the goroutine to feed data
}

[output]
-- main()    --
-- goroutine --

Unbuffered channels also called synchronous channels.

The program above will print both messages without any probability. All operations on unbuffered channels block until both sender and receiver are ready to communicate. In the case above , the program will ends (<-done succeed to execute) if the goroutine feed data to the channel and unblock the main program.

b) l_am_full.go

package main

import "fmt"

func eat(ch chan int) {
    for i:= 0; i < 3; i++ {
        ch <- i
    }
}

func main() {
    c := make(chan int)
    go eat(c)               // after eating 0 , l am full
    fmt.Println(<-c)        // print 0 only
}

[output]
0

c) shit_smoothly.go ( unblock it , take some laxatives ; ) )

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    go eat(c)
    go shit(c)
    time.Sleep(1e9)
}

func eat(ch chan int) {
    for i:= 0; i < 3; i++ {
        ch <- i
    }
}

func shit(ch chan int) {
    for {
        fmt.Println(<-ch)
    }
}

[output]
0
1
2

In this case , you are doing two things concurrently , shitting when you are eating.

3. Asynchronous (non-blocking) channels

We can make a asynchronous channel by creating a buffered channel, Let's compare both the synchronous & asynchronous channel :

shit_blood.go (unbuffered, synchronous channel)

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)             // unbuffered, synchronous channel
    go eat(c)
    time.Sleep(time.Second * 2)
    shit(c)
}

func eat(ch chan int) {
    for i:= 0; i < 3; i++ {
        fmt.Println("bleeding")
        ch <- i                     // code block here after 1st "bleeding" !
    }
}

func shit(ch chan int) {
    for i:= 0; i < 3; i++{
        fmt.Println(<-ch)
    }
}

[output]
bleeding                            // 1st bleeding
0
bleeding
bleeding
1
2

bleeding_and_shit.go (buffered, asynchronous channel)

If we change the code to

c := make(chan int,3)             // buffered, asynchronous channel

then we get the different output :

bleeding
bleeding
bleeding
0
1
2

Here we saw that all writing operations are performed without waiting (asynchronous behaviour) for the first read, the buffer of the channel allows to store all three shits (0,1,2). By changing channels capacity we can control the amount of information being processed thus limiting throughput of a system.

Reference :

  1. Golang channels tutorial By Guz Alexander
  2. Go Concurrency Patterns: Pipelines and cancellation

results matching ""

    No results matching ""