YouTip LogoYouTip

Go Concurrent

Go Concurrency \n\n

Concurrency refers to the ability of a program to execute multiple tasks simultaneously.

\n\n

Go supports concurrency, providing a concise and efficient way to implement it through goroutines and channels.

\n\n

Goroutines:

\n\n
    \n
  • The unit of concurrent execution in Go, similar to lightweight threads.
  • \n
  • Goroutine scheduling is managed by the Go runtime; users do not need to manually allocate threads.
  • \n
  • Use the go keyword to start a Goroutine.
  • \n
  • Goroutines are non-blocking and can efficiently run thousands of them.
  • \n
\n\n

Channel:

\n\n
    \n
  • A mechanism in Go for communication between Goroutines.
  • \n
  • Supports synchronization and data sharing, avoiding explicit locking mechanisms.
  • \n
  • Created using the chan keyword; data is sent and received using the <- operator.
  • \n
\n\n

Scheduler:

\n\n

Go's scheduler is based on the GMP model. It assigns Goroutines to system threads for execution and efficiently manages concurrency through the coordination of M and P.

\n\n
    \n
  • G: Goroutine.
  • \n
  • M: System thread (Machine).
  • \n
  • P: Logical processor (Processor).
  • \n
\n\n
\n\n

Goroutine

\n\n

A goroutine is a lightweight thread, and its scheduling is managed by the Go runtime.

\n\n

Goroutine syntax format:

\n\n
go function name( parameter list )
\n\n

For example:

\n\n
go f(x, y, z)
\n\n

Start a new goroutine:

\n\n
f(x, y, z)
\n\n

Go allows using the go statement to start a new runtime thread, i.e., a goroutine, to execute a function in a different, newly created goroutine. All goroutines in the same program share the same address space.

\n\n

Example

\n\n
package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc sayHello() {\n    for i := 0; i < 5; i++ {\n        fmt.Println("Hello")\n        time.Sleep(100 * time.Millisecond)\n    }\n}\n\nfunc main() {\n    go sayHello() // Start Goroutine\n    for i := 0; i < 5; i++ {\n        fmt.Println("Main")\n        time.Sleep(100 * time.Millisecond)\n    }\n}
\n\n

Executing the above code, you will see the output of Main and Hello. The output has no fixed order because they are executed by two goroutines:

\n\n
Main\nHello\nMain\nHello\n...
\n\n
\n\n

Channel

\n\n

Channels are used for data transfer between Goroutines.

\n\n

Channels can be used to synchronize execution and communicate between two goroutines by passing a value of a specified type.

\n\n

Use the make function to create a channel, and use the <- operator to send and receive data. If no direction is specified, it is a bidirectional channel.

\n\n
ch <- v    // Send v to channel ch\nv := <-ch  // Receive from ch and assign value to v
\n\n

Declaring a channel is simple; we use the chan keyword. Channels must be created before use:

\n\n
ch := make(chan int)
\n\n

Note: By default, channels are unbuffered. The sender sends data, and there must be a corresponding receiver to receive the data.

\n\n

The following example calculates the sum of numbers using two goroutines. After the goroutine finishes the calculation, it calculates the sum of the two results:

\n\n

Example

\n\n
package main\n\nimport "fmt"\n\nfunc sum(s []int, c chan int) {\n    sum := 0\n    for _, v := range s {\n        sum += v\n    }\n    c <- sum // Send sum to channel c\n}\n\nfunc main() {\n    s := []int{7, 2, 8, -9, 4, 0}\n    c := make(chan int)\n    go sum(s[:len(s)/2], c)\n    go sum(s[len(s)/2:], c)\n    x, y := <-c, <-c // Receive from channel c\n    fmt.Println(x, y, x+y)\n}
\n\n

The output is:

\n\n
-5 17 12
\n\n

Channel Buffer

\n\n

Channels can have a buffer, specified by the second parameter of make:

\n\n
ch := make(chan int, 100)
\n\n

Buffered channels allow the sender's data sending and the receiver's data retrieval to be asynchronous. This means the data sent by the sender can be placed in the buffer, waiting for the receiver to retrieve it, rather than requiring the receiver to get the data immediately.

\n\n

However, since the buffer size is limited, there must still be a receiver to receive the data. Otherwise, once the buffer is full, the sender cannot send any more data.

\n\n

Note: If the channel is unbuffered, the sender blocks until the receiver receives the value from the channel. If the channel is buffered, the sender blocks until the sent value is copied into the buffer; if the buffer is full, it means waiting until some receiver obtains a value. The receiver blocks until there is a value to receive.

\n\n

Example

\n\n
package main\n\nimport "fmt"\n\nfunc main() {\n    // Here we define a buffered channel that can store integers\n    // with a buffer size of 2\n    ch := make(chan int, 2)\n\n    // Because ch is a buffered channel, we can send two data simultaneously\n    // without needing to synchronize the read immediately\n    ch <- 1\n    ch <- 2\n\n    // Retrieve these two data\n    fmt.Println(<-ch)\n    fmt.Println(<-ch)\n}
\n\n

The output is:

\n\n
1\n2
\n\n

Go Traversing and Closing Channels

\n\n

Go uses the range keyword to traverse data received, similar to arrays or slices. The format is:

\n\n
v, ok := <-ch
\n\n

If the channel cannot receive data, ok becomes false, and the channel can be closed using the close() function.

\n\n

Example

\n\n
package main\n\nimport (\n    "fmt"\n)\n\nfunc fibonacci(n int, c chan int) {\n    x, y := 0, 1\n    for i := 0; i < n; i++ {\n        c <- x\n        x, y = y, x+y\n    }\n    close(c)\n}\n\nfunc main() {\n    c := make(chan int, 10)\n    go fibonacci(cap(c), c)\n\n    // The range function traverses each data received from the channel.\n    // Because c closes the channel after sending 10 data,\n    // the range function here ends after receiving 10 data.\n    // If the above c channel is not closed, the range function will not\n    // end and will block when receiving the 11th data.\n    for i := range c {\n        fmt.Println(i)\n    }\n}
\n\n

The output is:

\n\n
0\n1\n1\n2\n3\n5\n8\n13\n21\n34
\n\n

Select Statement

\n\n

The select statement allows a goroutine to wait on multiple communication operations. select blocks until one of the cases can proceed:

\n\n

Example

\n\n
package main\n\nimport "fmt"\n\nfunc fibonacci(c, quit chan int) {\n    x, y := 0, 1\n    for {\n        select {\n        case c <- x:\n            x, y = y, x+y\n        case <-quit:\n            fmt.Println("quit")\n            return\n        }\n    }\n}\n\nfunc main() {\n    c := make(chan int)\n    quit := make(chan int)\n    go func() {\n        for i := 0; i < 10; i++ {\n            fmt.Println(<-c)\n        }\n        quit <- 0\n    }()\n    fibonacci(c, quit)\n}
\n\n

In the above code, the fibonacci goroutine sends the Fibonacci sequence on channel c and exits when it receives a signal from the quit channel.

\n\n

The output is:

\n\n
0\n1\n1\n2\n3\n5\n8\n13\n21\n34\nquit
\n\n
\n\n

Using WaitGroup

\n\n

sync.WaitGroup is used to wait for multiple Goroutines to complete.

\n\n

Synchronizing Multiple Goroutines:

\n\n

Example

\n\n
package main\n\nimport (\n    "fmt"\n    "sync"\n)\n\nfunc worker(id int, wg *sync.WaitGroup) {\n    defer wg.Done() // Call Done() when Goroutine completes\n    fmt.Printf("Worker %d startedn", id)\n    fmt.Printf("Worker %d finishedn", id)\n}\n\nfunc main() {\n    var wg sync.WaitGroup\n    for i := 1; i <= 3; i++ {\n        wg.Add(1) // Increment counter\n        go worker(i, &wg)\n    }\n    wg.Wait() // Wait for all Goroutines to complete\n    fmt.Println("All workers done")\n}
\n\n

The output of the above code is:

\n\n
Worker 1 started\nWorker 1 finished\nWorker 2 started\nWorker 2 finished\nWorker 3 started\nWorker 3 finished\nAll workers done
\n\n
\n\n

Advanced Features

\n\n

Buffered Channel:

\n\n

Create a buffered Channel.

\n\n
ch := make(chan int, 2)
\n\n

Context:

\n\n

Used to control the lifecycle of Goroutines.

\n\n

context.WithCancel, context.WithTimeout.

\n\n

Mutex and RWMutex:

\n\n

sync.Mutex provides a mutual exclusion lock to protect shared resources.

\n\n
var mu sync.Mutex\nmu.Lock() // critical section\nmu.Unlock()
\n\n
\n\n

Concurrency Programming Summary

\n\n

Go provides powerful concurrency support through Goroutines and Channels, simplifying the complexity of traditional thread models. Combined with schedulers and synchronization tools, high-performance concurrent programs can be easily implemented.

\n\n
    \n
  • Goroutines are lightweight threads, started using the go keyword.
  • \n
  • Channels are used for communication between goroutines.
  • \n
  • Select Statement is used to wait on multiple channel operations.
  • \n
\n\n

Common Issues

\n\n

Deadlock:

\n\n
    \n
  • Example: All Goroutines are waiting, but no data is available.
  • \n
  • Solution: Avoid infinite waiting; close channels correctly.
  • \n
\n\n

Data Race:

\n\n
    \n
  • Example: Multiple Goroutines access the same variable simultaneously.
  • \n
  • Solution: Use Mutex or Channel to synchronize access.
  • \n
← Dom HtmlcollectionPython3 Func Chr Html β†’