Explicit cancellation in Golang Concurrency Problems
What is Explicit cancellation?
Imagine you ask your friend to fetch something from the store, but halfway there, you realize you don’t need it anymore. Explicit cancellation is like calling your friend and telling them to stop because you changed your mind. In Go, it’s like telling a piece of code to stop what it’s doing because you don’t need the result anymore or because you want to do something else.
What is the case? why we need this?
Explicit cancellation in Go is crucial for managing resources efficiently and ensuring the smooth operation of concurrent programs. Here’s why it’s necessary:
1. Preventing Resource Leaks: When goroutines continue running unnecessarily, they consume memory and other runtime resources. If these goroutines hold references to data, it can prevent that data from being garbage collected, leading to memory leaks. Explicit cancellation allows you to stop these goroutines when they are no longer needed, preventing resource leaks.
2. Ensuring Timely Termination : In complex programs, it’s common for certain tasks or operations to become unnecessary or fail prematurely. Without explicit cancellation, these operations may continue indefinitely, potentially causing the program to hang or waste resources. Explicit cancellation ensures that these operations terminate in a timely manner.
3. Graceful Shutdown : In applications like servers or long-running processes, it’s essential to have a way to gracefully shut down operations. Explicit cancellation provides a mechanism to signal to running goroutines that they should finish what they are doing and exit, ensuring a smooth shutdown process without abruptly terminating tasks.
4. Managing Dependency Failures : In pipelines or concurrent workflows, upstream stages may need to stop producing values if downstream stages encounter errors or terminate early. Explicit cancellation allows you to propagate these cancellation signals upstream, stopping unnecessary work and preventing the accumulation of unused data.
5. Improving Performance : By stopping unnecessary operations promptly, explicit cancellation can improve the performance of concurrent programs. It reduces the overhead of managing unnecessary goroutines and allows resources to be reallocated more efficiently.
We will see a simple example to Explicit cancellation in Golang Concurrency Problems by sending values on a channel called done.
Problem statement
We have a producer goroutine that generates a stream of 10 numbers, and we need to consume only one of these numbers. Once we’ve consumed the number, we want to send a message upstream indicating that we’ve finished processing the data.
let create a function that produce 10 numbers and sends it to channel
func Producer(ch chan int){
for i := 0;i<=10;i++{
ch<-i
}
close(ch)
}
Now consume the channel for single time
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go Producer(ch)
fmt.Println(<-ch)
}
You can see that after consuming one time main goroutine will be return and the memory leak will be happen(in real world large applications in such cases) because we didn’t explicitlty tell the upstream to stop the sending data to that channel.
Now lets manage it with the help of done.
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
done := make(chan struct{}) // empty struct for memory efficiency
defer close(done)
go Producer(done, ch)
fmt.Println(<-ch)
done<-struct{}{}
}
Note : Empty struct don’t consume any additional memory beyond what is required for the struct type itself
Channel Creation:
ch := make(chan int) : Creates a channel named ch of type int. This channel will be used to send integers from the producer goroutine to the consumer goroutine.
done := make(chan struct{}) : Creates a channel named done of type struct{}. This channel is used as a signal to tell the producer goroutine to stop producing data. It’s often used for signaling without passing any actual data, which helps in memory efficiency.
Deferred Channel Closure :
defer close(done): Defers the closure of the done channel until the end of the main() function. This ensures that the done channel is closed after all operations in the main function, even if there’s a panic or an early return.
Goroutine Creation:
go Producer(done, ch): Starts a new goroutine that calls the Producer function, passing the done channel and the ch channel as arguments. This goroutine will produce data and send it to the ch channel until it receives a signal on the done channel.
Consuming Data:
fmt.Println(<-ch): Receives data from the ch channel and prints it to the console. This line blocks until data is available on the channel.
Modify Producer Function :
func Producer(done chan struct{},ch chan int){
for i := 0;i<=10;i++{
select{
case ch<-i :
case <-done:
return
}
}
close(ch)
}
select { : Starts a select statement to handle channel operations.
case ch <- i : Tries to send the value of i into the ch channel. If the channel is not blocked (i.e., there’s a receiver ready to receive), it sends i. If the channel is blocked, it waits until the channel is ready to receive.
case <-done : Listens for a signal on the done channel. If a signal is received on done, it returns from the function, effectively stopping the production of data.
This can be also achieve using context package of golang. i will also demonstrate that in another blog. You can suggest me any topic related to golang for the blog. thanks.