A thread is the smallest unit of execution within a process, and it represents a single sequence of instructions that can be scheduled to run independently by the operating system.
Multithreading is a programming and execution model that allows multiple threads to exist within the context of a single process.
Within Go, lightweight threads, known as goroutines, are often used. These differ from traditional threads in the following ways:
Goroutines are managed by the Go runtime whereas threads are managed by the operating system
Goroutines can be thought of as an abstraction over threads making them computationally cheaper than threads
Goroutines run in the same address space, so access to shared memory must be synchronized
Goroutines
Goroutines can be created by just using the keyword go in front of the function being called.
package main
import "time"
func Order(food string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond);
println("Order received: " + food);
}
}
func main(){
go Order("Ice cream");
Order("Pizza");
}
If we remove the keyword go then we can predict a defined order .i.e Ice cream
will be printed first followed by Pizza
With the keyword go however, Ice cream
and Pizza
can be printed in any order before or after each other depending upon the execution times
Communication between Goroutines
Channels
Channels are a typed conduit through which you can send and receive values with the channel operator, <-
.
package main
import "time"
func Receive(ch chan int){
for i := 0; i < 5; i++ {
v := <- ch;
println(v);
}
}
func Send(ch chan int){
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond);
ch <- i;
}
}
func main(){
ch := make(chan int);
go Send(ch);
Receive(ch);
}
The above code will output numbers 0-4 in intervals of 100 ms. The integers are passed between goroutines using channel ch
v := <- ch // Receives data and stores it in `v`
ch <- i // Sends `i`
By default, sends and receives are blocking in nature until the other side is ready. This allows goroutines to synchronise without explicit locks or condition variables.
Buffered Channels
Channels can also be buffered by initialising it with buffer length.
package main
import "time"
func Receive(ch chan int){
for i := 0; i < 5; i++ {
v := <- ch;
println(v);
}
}
func Send(ch chan int){
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond);
ch <- i;
}
}
func main(){
ch := make(chan int, 10);
go Send(ch);
Receive(ch);
}
make(chan int, 10)
initialises a buffer of length 10. Sends to a buffered channel are blocking in nature only when the buffer is full. Receives are blocking in nature when the buffer is empty.
Synchronisation of Goroutines
Mutex
Go's standard library provides mutual exclusion with sync.Mutex
and its two methods:
Lock
Unlock
We can define a block of code to be executed in mutual exclusion by surrounding it with a call to Lock
and Unlock
package main
import (
"sync"
"time"
)
type Counter struct {
m sync.Mutex
count int
}
func (c *Counter) increment() {
c.m.Lock();
c.count++;
c.m.Unlock();
}
func (c *Counter) decrement() {
c.m.Lock();
c.count--;
c.m.Unlock();
}
func main(){
counter := Counter{count: 0};
for i := 0; i<500; i++ {
go counter.increment();
}
for i := 0; i<400; i++ {
go counter.decrement();
}
time.Sleep(1*time.Second);
println(counter.count);
}
Both functions increment
and decrement
acquire a lock on the counter before modifying its value. Hence, we get a predictable output of 100.
Wait Groups
A WaitGroup waits for a collection of goroutines to finish.
There are 3 key methods that are provided by sync.WaitGroup
:
Add
sets the number of goroutines to wait forDone
decrements the WaitGroup counter by oneWait
is blocking in nature until the WaitGroup counter is zero.
package main
import (
"sync"
"time"
)
func timedloop() {
time.Sleep(3*time.Millisecond);
println("Done: timedloop");
}
func main() {
var wg sync.WaitGroup;
for i := 0; i<3; i++ {
wg.Add(1);
go func () {
defer wg.Done();
timedloop();
}()
}
wg.Wait();
println("Done");
}
WaitGroup ensures that the main
goroutine has to wait until all the other goroutines are finished.
The output comes out as follows:
Done: timedloop
Done: timedloop
Done: timedloop
Done