Introducing Concurency

Introducing Concurency

October 7, 2023 by RedVelvet

In computing, concurrency is the ability to handle multiple tasks at once. Concurrency, simply put, is about tasks that start, run and complete in overlapping time periods, in no specific order.

Education is the most powerful weapon which you can use to change the world. - Nelson Mandela -

Concurrency Vs Parallelism

Parallel computing or parallelism, often confused with concurrency, is the simultaneous execution of tasks at runtime and it requires hardware with multiple computing resources.

Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once. - Rob Pike -

Goroutines

Goroutines, are the functions that can run concurrently with the rest of the code. Goroutines are dissimilar to threads in the fact that goroutines are less expensive to create, can communicate with each other using channels and they can run on a single OS thread.

To create a goroutine, create a normal function and add the go keyword when calling it.

Concurency
func nowthatshot() {

	fmt.Println("maybe slow")

	time.Sleep(1 * time.Second)

	fmt.Println("fine im done")

	}

func main() {

	fmt.Println("start")
	// Create a goroutine to do something slow 
	go nowthatshot()
	fmt.Println("waiting for the slow func") 

	time.Sleep(500 * time.Millisecond)

	fmt.Println("Finished the main task!") 
	time.Sleep(1 * time.Second)
	}

Result

start
waiting for the slow func
maybe slow
Finished the main task!
fine im done

Channels

If we talk about concurrency in Golang, Golang provides us with a type of concurrency called Channel. Simply put channels help communicate between Goroutines. We can send and receive information from one Goroutine to the other.

Go has two types of channels:

By default channels are unbuffered, meaning that they will only accept sends (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value. Buffered channels accept a limited number of values without a corresponding receiver for those values.

Unbuffered Channels

A channel is a communication vessel used by goroutines to interact with one another. As stated before by default Channels are unbuffered, meaning they cannot hold information. Consequently, if an unbuffered channel receives some data it has to immediately send it somewhere.

Let’s look at this example

Unbuffered Channel

import "fmt"

func main(){

	ch := make(chan int) // Making a channel that takes in integers

	ch <- 10 // the value 10 is sent to the channel, this the channel's sender

	v := <- ch // the variable v receives the channel's value, this is the receiver

	fmt.Println("Received", v)
}

The program will get stuck on the channel send operation waiting forever for a goroutine to access it.

This is called a deadlock, a deadlock happens when a group of goroutines are waiting for each other and none of them is able to proceed.

It’s like playing hot potato but you’re the only person playing with no one to hand the potato to, you would lose immediately.

A workaround deadlock is to add a goroutine

Unbuffered Channel + Goroutine
import "fmt"

func main(){

	ch := make(chan int) // Making a channel that takes in integers

	// Created a goroutine that contains the sender 
	go func(){
		ch <- 10 
	}()

	v := <- ch // the variable v receives the channel's value, this is the receiver
	fmt.Println("Received", v)
}
⚠️

In the above code snippet, we only have 1 goroutine and channels require 2 goroutines to operate. Well, the code does actually contain 2 goroutines. The first is the go func and the second is the generated goroutine.

Buffered Channels

Unlike unbuffered channels, buffered channels have a capacity attribute, meaning they can hold an n number of values.

Buffered Channels

import "fmt"

func main(){

	ch := make(chan int, 1) // Created a buffered int channel that can hold one value
	
	fmt.Println("Sender")
	ch <- 10 //Creating a sender

	v := <- ch //Creating a receiver
	fmt.Println("Received", v)

}

Contrary to unbuffered channels, the code snippet won’t result in a deadlock. This is because the sender of a buffered channel only blocks when the channel is full - while the receiver only blocks when the channel is empty.

The added attribute of buffered channels creates a new problem, let’s look at it with this simple example.


import "fmt"

func main(){

	ch := make(chan int, 1)// Creating a buffered int channel capable of holding one value

	fmt.Println("First Sender")
	
	ch <- 10

	fmt.Println("Second Sender")
	
	ch <- 11
	
	fmt.Println("First Receiver: ", <- ch)
	fmt.Println("Second Receiver: ", <- ch)

}

Running the code will result in a deadlock, the sender is infinitely blocked. This is because the channel is full and got sent another value before the channel is freed up.

This problem can be solved by either increasing the capacity of the channel

Effect of Capacity

package main 

import "fmt"

func main(){

	ch := make(chan int, 2)// Creating a buffered int channel this time capable of holding two values

	fmt.Println("First Sender")
	
	ch <- 10

	fmt.Println("Second Sender")
	
	ch <- 11
	
	fmt.Println("First Receiver: ", <- ch)
	fmt.Println("Second Receiver: ", <- ch)

}
ℹ️

How it handles multiple data The buffered channel data is stored in a FIFO queue meaning the first value sent to the channel is the first one to leave the channel.

The problem could also be solved by adding a Goroutine

Buffered Channel + Goroutine
package main

import "fmt"

func main() {

	ch := make(chan int, 1) // Making a channel that takes in integers

	// Created a goroutine that contains the sender

	go func() {
		fmt.Println("Sent: 10")
		ch <- 10
		fmt.Println("Sent: 11")
		ch <- 11
	}()

	v := <-ch

	fmt.Println("Received", v)

	v = <-ch

	fmt.Println("Recived", v)
}

Got It

Concurrency is one of the strengths that make Go so powerful and popular. Goroutines are the power of concurrency in go, they are functions that can run concurrently. Channels are how these goroutines can send and receive data between each other.

Last updated on