Goroutine Basics
Goroutine
When asked to articulate the advantages of Golang to Gophers, the topic of Concurrency frequently arises. The foundation of this concept lies in goroutines, which are designed to be lightweight and straightforward to manage. A brief overview of this topic has been compiled herein.
Concurrency vs. Parallelism
Prior to comprehending goroutines, it is pertinent to clarify two frequently conflated concepts.
- Concurrency: Concurrency pertains to the management of multiple tasks over a period of time. It does not necessarily imply simultaneous execution; rather, it is a structural and logical paradigm where multiple tasks are divided into smaller units and executed alternately, thereby creating the appearance of simultaneous processing to the user. Concurrency is achievable even on a single-core system.
- Parallelism: Parallelism signifies the simultaneous execution of multiple tasks across multiple cores. It literally means that tasks proceed in parallel, executing different operations concurrently.
Goroutines facilitate the straightforward implementation of concurrency through the Go runtime scheduler and naturally leverage parallelism via the GOMAXPROCS
setting.
Java's multithreading, commonly characterized by high utilization, represents a quintessential example of parallelism.
Why are Goroutines advantageous?
Lightweight
The cost of creation for goroutines is remarkably low compared to other languages. A pertinent question arises as to why Golang utilizes less overhead; this is because their creation location is managed internally within the Go runtime. This is attributable to their nature as lightweight logical threads, which are smaller than OS thread units. An initial stack size of approximately 2KB is required, and the stack can dynamically vary by adding more space according to the user's implementation.
Managed at the stack unit level, creation and removal are exceptionally fast and inexpensive, enabling the processing of millions of goroutines without significant burden. Consequently, Goroutines can minimize OS kernel intervention due to the runtime scheduler.
High Performance
Firstly, as previously explained, Goroutines involve minimal OS kernel intervention. Context switching at the User-Level is less costly than at the OS thread level, facilitating rapid task transitions.
Furthermore, it employs an M:N model to allocate and manage OS threads. By establishing an OS thread pool, processing can be achieved with fewer threads, obviating the need for numerous threads. For instance, if a goroutine enters a waiting state, such as during a system call, the Go runtime executes another goroutine on the OS thread, thereby preventing the OS thread from idly waiting and efficiently utilizing the CPU for rapid processing.
This characteristic enables Golang to achieve superior performance in I/O operations compared to other languages.
Concise
A significant advantage is the ability to easily manage functions with a single go
keyword when concurrency is required.
In contrast, complex locks such as Mutex
and Semaphore
necessitate their use, which in turn compels consideration of deadlock states, an unavoidable consequence of using locks. This often leads to intricate stages from the initial design phase of development.
Goroutines advocate data transmission via Channels
, adhering to the philosophy "Do not communicate by sharing memory; instead, share memory by communicating." Furthermore, SELECT
combined with channels supports the functionality to process data from the first ready channel. Additionally, sync.WaitGroup
allows for straightforward waiting until all goroutines complete, simplifying workflow management. These tools help prevent data contention issues between threads and enable safer concurrent processing.
Moreover, by utilizing context
, the lifecycle, cancellation, timeout, deadline, and scope of requests can be controlled at the user level, thereby ensuring a certain degree of stability.
Parallel Operations of Goroutine (GOMAXPROCS)
While the benefits of goroutine concurrency have been discussed, you might wonder if parallelism is supported. The number of CPU cores has increased significantly compared to the past, often exceeding double digits, and even home PCs now feature a considerable number of cores.
However, Goroutine does indeed perform parallel operations, facilitated by GOMAXPROCS
.
If GOMAXPROCS
is not explicitly configured, its value is set differently depending on the Go version.
Prior to 1.5: The default value was 1. If more than 1 was required, it was mandatory to set it using methods such as
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 to 1.24: This was changed to all available logical cores. From this version onward, developers generally did not need to configure it unless specific constraints were required.
1.25: As an acclaimed language in containerized environments, it checks cGroup on Linux to identify the
CPU limit
configured for the container.Therefore, if the number of logical cores is 10 and the CPU limit is 5,
GOMAXPROCS
will be set to the lower value of 5.
The modification in version 1.25 represents a significant enhancement, particularly increasing the language's utility in containerized environments. This update helps prevent CPU throttling by reducing unnecessary thread creation and context switching.
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "time"
8)
9
10func exe(name int, wg *sync.WaitGroup) {
11 defer wg.Done()
12
13 fmt.Printf("Goroutine %d: ์์\n", name) // Goroutine %d: Start
14 time.Sleep(10 * time.Millisecond) // ์์
์๋ฎฌ๋ ์ด์
์ ์ํ ์ง์ฐ (Delay for task simulation)
15 fmt.Printf("Goroutine %d: ์์\n", name) // Goroutine %d: Start
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU ์ฝ์ด 2๊ฐ๋ง ์ฌ์ฉ (Use only 2 CPU cores)
20 wg := sync.WaitGroup();
21 goroutineCount := 10
22 wg.Add(goroutineCount)
23
24 for i := 0; i < goroutineCount; i++ {
25 go exe(i, &wg)
26 }
27
28 fmt.Println("๋ชจ๋ goroutine์ด ๋๋ ๋๊น์ง ๋๊ธฐํฉ๋๋ค...") // Waiting for all goroutines to finish...
29 wg.Wait()
30 fmt.Println("๋ชจ๋ ์์
์ด ์๋ฃ๋์์ต๋๋ค.") // All tasks completed.
31
32}
33
Goroutine Scheduler (M:N Model)
Expanding upon the previous discussion regarding the M:N model's allocation and management of OS threads, a more detailed examination reveals the goroutine GMP model.
- G (Goroutine): The smallest unit of work executed in Go.
- M (Machine): OS Thread (the actual location of work execution).
- P (Processor): A logical processor managed by the Go runtime.
Additionally, P possesses a Local Run Queue and functions as a scheduler, assigning allocated Gs to Ms. In simple terms, a goroutine operates as follows:
The operational process of GMP is as follows:
- When a G (Goroutine) is created, it is assigned to the Local Run Queue of a P (Processor).
- The P (Processor) assigns the G (Goroutine) from its Local Run Queue to an M (Machine).
- The M (Machine) returns the status of the G (Goroutine), which can be blocked, completed, or preempted.
- Work-Stealing: If a P's Local Run Queue becomes empty, another P checks the Global Queue. If no G (Goroutine) is found there either, it "steals" work from another Local P (Processor) to ensure all Ms remain active.
- System Call Handling (Blocking): If a G (Goroutine) experiences a block during execution, the M (Machine) enters a waiting state. At this point, the P (Processor) detaches from the blocked M (Machine) and associates with another M (Machine) to execute the next G (Goroutine). This mechanism prevents CPU wastage even during I/O operation wait times.
- If a single G (Goroutine) is preempted for an extended duration, it yields execution opportunities to other Gs (Goroutines).
Golang's GC (Garbage Collector) also operates on top of Goroutines, enabling parallel memory cleanup with minimal interruption to application execution (STW), thereby efficiently utilizing system resources.
Finally, Golang boasts a strong advantage among languages, and there are many more. I hope many developers enjoy using Go.
Thank you.