Concurrency in Go

Concurrency in Go

concurrency, routines, CSP and channels

Go Routines

Go routine is a light weighted thread of execution.

Routines differ from OS threads because the Go Scheduler can multiplex multiple go routines into a single OS thread.

They are a way to run a function concurrently (or parallelly ) with the main thread execution. We can use the go keyword before the function call to run it concurrently.

For eg: go functionName()

Go Paradigm of thinking about concurrency differs from other languages (It is based on Communicating Sequential processing ( CSP )

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

What does that even mean?

The general paradigm of concurrency in other languages ( like Java ) is to lock the shared variable while one of the threads is operating on it. Instead of explicitly using locks to mediate between go routines, go recommends the use of channels to pass references to data between goroutines. This ensures only one of the routines is accessing the data at a given time.

Though locks (mutex) are present in go for use, but that approach is not recommended.

Figure out: How does that help us improve Go performance?

Channels

Channels are medium to transfer data references between go routines. Channels are of two types buffered and unbuffered.

To send a value into the channel we use the <- ( left arrow going into the channel variable)

To receive a value from the channel we use the <- but before the variable name(going out of the channel name)

package main

import "fmt"

func main() {
    ch := make(chan string)

    go routineFunction(ch)
    recd := <-ch

    fmt.Println("Receiver: Hey this is what I recd! =>\n", recd)
}

func routineFunction(ch chan string) {
    sendVal := "Sender: Hey I am sending a value!"
    ch <- sendVal
}

Output:

Receiver: Hey this is what I recd! =>
 Sender: Hey I am sending a value!

Channels: Unbuffered vs Buffered

Unbuffered channels

Channels which are not defined with any size are unbuffered.

In the case of such channels, the routine waits for both the sender and receiver to be ready to transfer data. The unbuffered channel can store at max one value at a time and the value should be consumed by some other routine else the execution is waiting.

Buffered channels

Buffered channels have a given size hence one or more value can be written to the channel.

For such the sender and the receiver don't need to be ready. The sender can write the values in the channel which can be consumed by the receiver later.

In the below example, we define a buffered channel with size 4 (upto 4 values can be written into the channel ) in the below manner. After 4 values the channel values should be consumed.

ch := make(chan int,4)
ch <- 5
ch <- 3
ch <- 2
ch <- 1

In these channels, the values can be consumed by the other routine even after the channel is closed.

To close a channel we can close it using the close statement close(ch)

References

https://go.dev/blog/codelab-share

https://granulate.io/blog/deep-dive-into-golang-performance/

https://www.youtube.com/watch?v=zJd7Dvg3XCk

https://www.velotio.com/engineering-blog/understanding-golang-channels