Concurrency in Go - Part 1
Concurrency refers to the ability to perform multiple tasks simultaneously. Go
provides rich support for concurrency using lightweight threads called goroutines and communication bridges called channels.
A goroutine is an independent function that executes simultaneously in a separate lightweight thread managed by Go.
An example of a goroutine
package main
import (
"fmt"
"time"
)
func main() {
runGoRoutineSample()
}
func runGoRoutineSample() {
go helloworld()
time.Sleep(1 * time.Second)
goodbye()
}
func helloworld() {
fmt.Println("Hello World!")
}
func goodbye() {
fmt.Println("Good Bye!")
}
In this example, the main
goroutine invokes the helloworld()
function as a goroutine. The helloworld
goroutine starts executing concurrently with the main
goroutine. After a 1 second delay, the main
goroutine invokes the goodbye()
function.
The above example is a simple one and does not really help is in visualizing any overlap and thus the concurrency.
Overlapping goroutine example for visualization
package main
import (
"fmt"
"time"
)
func main() {
runGoRoutineSample(10)
}
func runGoRoutineSample(iterations int) {
// Starting Block
fmt.Print("Starting At: ")
fmt.Println(time.Now())
// Will Execute Immediately in a separate goroutine
go helloworld(iterations)
// Sleep 1 second to see the overlap
fmt.Println("From main goroutine: Sleeping for a second")
time.Sleep(1 * time.Second)
// Will Execute Immediately in main goroutine
goodbye()
// Wait in main goroutine to let the helloworld run
time.Sleep(15 * time.Second)
// Finishing Block
fmt.Print("Finished At: ")
fmt.Println(time.Now())
}
func helloworld(iterations int) {
for i := 0; i < iterations; i++ {
fmt.Printf("From goroutine: Hello %d At: %q, Now sleeping for a second", i, time.Now())
// Sleep 1 second after each print to see the overlap
time.Sleep(1 * time.Second)
}
// Format the output
fmt.Println()
}
func goodbye() {
// Format the output
fmt.Print("Good Bye! At : ")
fmt.Println(time.Now())
}
Understanding goodbye
Prints on console and logs the execution time
Exits
Understanding helloWorld
It uses a for loop to print messages on the console
It then sleeps for a second to allow goodbye to execute and show the overlap and goroutine parallelism
Understanding runGoRoutineSample
The function that simulates the overlapping routine behavior.
It starts with printing the execution start message and time.
It then invokes the
helloworld
function as a separate goroutine (on a separate lightweight thread)After invoking the new goroutine the execution continues in the
main
goroutine and thegoodbye
function is called.It then sleeps for some time to allow the
helloworld
function to finish.
Channels
Channels are used for communication between goroutines. They allow goroutines to send and receive data to/from each other.
They are key to Go's approach to concurrency and use Communicating Sequential Processes (CSP), a model proposed by Tony Hoare in 1978. CSP allows for concurrent processes to communicate through channels, avoiding the complexity and pitfalls of traditional multithreading and shared memory approaches.
Traditional concurrency models often involve intricate locking mechanisms that can lead to deadlocks, race conditions, and other synchronization issues. CSP, on the other hand, allows for clear and predictable communication patterns between concurrent processes, making it easier for developers to reason about and manage concurrency in their programs. By using CSP, Go enables developers to write highly concurrent programs that are both efficient and easy to understand.
Channel Example
package main
import (
"fmt"
"time"
)
func main() {
runner()
}
// Define the main runner function that orchestrates the simulation
func runner() {
// Create a channel to enable communication between goroutines
channel := make(chan string)
// Print a message to indicate the start of the simulation
fmt.Println("Starting the simulation!")
// Start the listener goroutine to receive messages on the channel
go listener(channel)
// Start the processor goroutine to send messages on the channel
go processor(channel)
// Pause the main goroutine to allow the processor to finish
fmt.Println("Main routine is sleeping to let processing finish!")
time.Sleep(5 * time.Second)
// Print a farewell message
fmt.Println("Good Bye!")
}
// Define the listener function that receives messages on the channel
func listener(channel chan string) {
// Print a message to indicate when the listener is invoked
fmt.Println("Listener has been invoked at " + time.Now().String())
// Receive a message from the channel
message := <-channel
// Print the received message along with the current time
fmt.Println("Listener received message: " + message + " , at " + time.Now().String())
}
// Define the processor function that sends messages on the channel
func processor(channel chan string) {
// Print a message to indicate when the processor receives the request
fmt.Println("Processor received the request at " + time.Now().String())
// Simulate processing time by pausing for some time
fmt.Println("Processor sleeping for a few second(s)!")
time.Sleep(2 * time.Second)
// Send a message over the channel to the listener
fmt.Println("Sending message over channel!")
channel <- "Proccessing Result!"
// Print a message to indicate when the processor finishes processing
fmt.Println("Sleeping for a second to simulate parallelism!")
time.Sleep(1 * time.Second)
fmt.Println("Processor has finished processing at " + time.Now().String())
}
In this example:
The main goroutine invokes the runner function
The runner creates a channel that allows string exchange between routines
The main goroutine invokes the listener and processor functions on new goroutines
The listener goroutine then waits to receive a message on the channel
The processor goroutine sends a message on the channel
The listener goroutine receives the message
Likes, shares, and comments are highly appreciated.🙏