Concurrency and Multithreading in Go: Advanced Level
\n\nConcurrency is one of the key features of the Go language, built into its architecture at the runtime and syntax level. Unlike many other languages where multithreading is implemented through complex libraries and requires manual thread management, Go offers lightweight goroutines and channels. At an advanced level, it is important not just to launch several tasks, but to synchronize them properly, avoid data races, and use system resources efficiently. In this article, we will cover advanced techniques for working with concurrency in Go.
\n\n1. Goroutines: Lifecycle Management and Priorities
\n\nGoroutines are lightweight threads managed by the Go runtime. They are launched using the go keyword. However, at an advanced level, you need to understand how to control their execution and completion.
Basic goroutine launch:
\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc worker(id int) {\n fmt.Printf("Worker %d starting\\n", id)\n time.Sleep(time.Second)\n fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n for i := 1; i <= 5; i++ {\n go worker(i)\n }\n time.Sleep(2 * time.Second) // Workaround: waiting for completion\n}\n\nProblem: in this example, we use time.Sleep for waiting, which is unreliable. At an advanced level, channels and sync.WaitGroup are used.
2. Synchronization with sync.WaitGroup
\n\nsync.WaitGroup allows you to wait for a specified number of goroutines to complete. This is a basic but powerful synchronization tool.
package main\n\nimport (\n "fmt"\n "sync"\n "time"\n)\n\nfunc worker(id int, wg *sync.WaitGroup) {\n defer wg.Done() // Decrement the counter upon completion\n fmt.Printf("Worker %d starting\\n", id)\n time.Sleep(time.Second)\n fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n var wg sync.WaitGroup\n for i := 1; i <= 5; i++ {\n wg.Add(1) // Increment the counter before launching\n go worker(i, &wg)\n }\n wg.Wait() // Block until the counter reaches zero\n fmt.Println("All workers completed")\n}\n\nImportant: wg.Add() must be called before launching the goroutine, otherwise a data race may occur. Use defer wg.Done() inside the goroutine to guarantee the counter is decremented even in case of a panic.
3. Channels: Buffering, Direction, and select
\n\nChannels are the primary mechanism for exchanging data between goroutines. At an advanced level, buffered channels, unidirectional channels, and the select statement are used.
3.1. Buffered Channels
\nBuffered channels allow sending data without immediate reception. This reduces blocking and increases throughput.
\npackage main\n\nimport "fmt"\n\nfunc main() {\n ch := make(chan int, 3) // channel with a buffer of 3 elements\n ch <- 1\n ch <- 2\n ch <- 3\n // ch <- 4 // blocking, as the buffer is full\n fmt.Println(<-ch)\n fmt.Println(<-ch)\n fmt.Println(<-ch)\n}\n\n3.2. Unidirectional Channels
\nFor type safety and explicit channel role specification, unidirectional channels are used: chan<- (send only) and <-chan (receive only).
package main\n\nimport "fmt"\n\nfunc sender(out chan<- int) {\n for i := 0; i < 5; i++ {\n out <- i\n }\n close(out)\n}\n\nfunc receiver(in <-chan int) {\n for val := range in {\n fmt.Println(val)\n }\n}\n\nfunc main() {\n ch := make(chan int)\n go sender(ch)\n receiver(ch)\n}\n\n3.3. The select Statement
\nselect allows a goroutine to wait on multiple channels simultaneously. This is