In this tutorial, we will learn about Channel synchronization in Golang using 4 best examples. Channel in Golang provide a safe way for Goroutines to share data, avoid race conditions and ensuring data integrity. Channel can enforce a sequence of operations by making Goroutines wait for the appropriate signal or data before proceeding.
What is Channel Synchronization?
In Golang, Channel synchronization is powerful concept that allows different Goroutines to communicate and coordinate their actions in synchronized manner. It let Goroutines to send signal to each other via channel ensuring that certain operations happen in a specific order or only when conditions are met. Let’s understand this with a real life example.
When we go to a restaurant and order some food, Waiter and Chef has to do the coordination in order to proceed and deliver the food on our table. So here Waiter and Chef can be think of two different Goroutine that does channel synchronization and both wait for each other’s action before proceeding.
Channel Synchronization Features
There are broadly two features of Channel synchronization. They are:
Blocking – When a Goroutine sends data on a channel, it will block until there is a receiver ready to receive that data. Similarly, when a Goroutine tries to receive data from a channel, it will block until there is a sender ready to send.
Ordering – Channels can be used to enforce a specific order of execution between different Goroutines. For example, a Goroutine might wait for data on one channel before sending data on another cannel.
Channel Synchronization in Golang: [4 Best Examples]
Also read: Concurrency in Golang with Best Examples
Now that we understand what Channel synchronization is and its features, let’s look at some of the examples to implement the concept. There is one similarity in below 4 examples and i.e the processing time taken to execute the Goroutines. If you do not have prior knowledge of what Goroutines are, please make yourself familiar by referring to How to create Goroutine in Golang with Examples. Notice that even though the approach if different in each example, the processing time taken by each example is approximately 2s.
Example-1: Use two goroutine to calculate cube root
In this example, we will calculate the cube root of a number and print the report once the calculation is done. To do so, we have created two functions, each converted into Goroutine. calCubeRoot function will calculate the cube root and printReport function will print the result of cube root.
package main import ( "fmt" "time" ) var num = 88 var result = 0 func main() { cuberootChan := make(chan int) reportChan := make(chan string) startTimer := time.Now() go calCubeRoot(num, cuberootChan) go printReport(cuberootChan, reportChan) <-reportChan stopTimer := time.Since(startTimer) fmt.Println("\nProcess took", stopTimer, "to complete") } func calCubeRoot(num int, cuberootChan chan int) { fmt.Println("Taking 2 seconds to calculate cube root.....") time.Sleep(time.Second * 2) result = num * num * num cuberootChan <- result } func printReport(cuberootChan chan int, reportChan chan string) { time.Sleep(time.Second * 1) cubeRootResult := <-cuberootChan fmt.Println("Result of cube root of", num, "is:", cubeRootResult) reportChan <- "Report generated" }
PS C:\User\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\hello-world.go Taking 2 seconds to calculate cube root..... Result of cube root of 88 is: 681472 Process took 2.0106997s to complete
Example-2: Use nested functions to calculate cube root
In this example, instead of using two separate functions, we now will create two nested functions inside main() function and do the same operation which we did in example-1.
package main import ( "fmt" "time" ) var num = 88 var result = 0 func main() { cuberootChan := make(chan int) reportChan := make(chan string) startTimer := time.Now() calCubeRoot := func() { //nested function fmt.Println("Taking 2 seconds to calculate cube root.....") time.Sleep(time.Second * 2) result = num * num * num cuberootChan <- result } printReport := func() { //nested function time.Sleep(time.Second * 1) cubeRootResult := <-cuberootChan fmt.Println("Result of cube root of", num, "is:", cubeRootResult) reportChan <- "Report generated" } go calCubeRoot() go printReport() <-reportChan // Wait for the report to be generated stopTimer := time.Since(startTimer) fmt.Println("\nProcess took", stopTimer, "to complete") }
PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\hello-world.go Taking 2 seconds to calculate cube root..... Result of cube root of 88 is: 681472 Process took 2.0037897s to complete
Example-3: Use one goroutine to calculate cube root
In this example, we will create one large Goroutine inside which entire process is put and executed.
package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() // Mark the wait group as done when the goroutine exits var num = 88 cuberootChan := make(chan int) reportChan := make(chan string) startTimer := time.Now() calCubeRoot := func() { fmt.Println("Taking 2 seconds to calculate cube root.....") time.Sleep(time.Second * 2) cubeRootResult := num * num * num // Calculate result directly cuberootChan <- cubeRootResult } printReport := func() { time.Sleep(time.Second * 1) cubeRootResult := <-cuberootChan fmt.Println("Result of cube root of", num, "is:", cubeRootResult) reportChan <- "Report generated" } go calCubeRoot() go printReport() <-reportChan // Wait for the report to be generated stopTimer := time.Since(startTimer) fmt.Println("\nProcess took", stopTimer, "to complete") }() wg.Wait() // Wait for the wait group to be done }
PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\hello-world.go Taking 2 seconds to calculate cube root..... Result of cube root of 88 is: 681472 Process took 2.0090586s to complete
Example-4: Use for loop to calculate cube root
In this example, We are using a for loop to run 1000 Goroutines concurrently. That means cube root now will be calculated for each number and printer till 1000 unlike how we saw in above examples where we were calculating the cube root for a specific number.
package main import ( "fmt" "sync" "time" ) func main() { startTimer := time.Now() var wg sync.WaitGroup for i := 1; i <= 1000; i++ { wg.Add(1) go func(j int) { defer wg.Done() // Mark the wait group as done when the goroutine exits cuberootChan := make(chan int) reportChan := make(chan string) calCubeRoot := func() { time.Sleep(time.Second * 2) result := j * j * j // Calculate result directly cuberootChan <- result } printReport := func() { time.Sleep(time.Second * 1) cubeRootResult := <-cuberootChan fmt.Println("Result of cube root of", j, "is:", cubeRootResult) reportChan <- "Report generated" } go calCubeRoot() go printReport() <-reportChan // Wait for the report to be generated }(i) // Pass i as a parameter to the goroutine } wg.Wait() // Wait for all goroutines to be done stopTimer := time.Since(startTimer) fmt.Println("\nProcess took", stopTimer, "to complete") }
PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\hello-world.go Result of cube root of 961 is: 887503681 Result of cube root of 979 is: 938313739 Result of cube root of 524 is: 143877824 Result of cube root of 67 is: 300763 Result of cube root of 68 is: 314432 Result of cube root of 14 is: 2744 ..................................... ..................................... ..................................... Result of cube root of 578 is: 193100552 Result of cube root of 52 is: 140608 Process took 2.1532056s to complete
Summary
Channel synchronization is a versatile tool for designing concurrent programs that operate harmoniously. By properly structuring goroutines and using channels, developers can effectively manage the flow of information and synchronization between parallel tasks, resulting in efficient and reliable concurrent applications.