Go coroutines revealed: the perfect combination of lightweight, concurrency and performance

Go coroutines provide powerful tools for concurrent programming, combined with lightweight and efficient features, bringing developers a unique programming experience. This article deeply discusses the basic principles, synchronization mechanism, advanced usage, performance and best practices of Go coroutines, aiming to provide readers with comprehensive and in-depth understanding and application guidance.

Follow the public account [TechLeadCloud] to share full-dimensional knowledge of Internet architecture and cloud service technology. The author has 10+ years of Internet service architecture, AI product development experience, and team management experience. He holds a master's degree from Tongji University in Fudan University, a member of Fudan Robot Intelligence Laboratory, a senior architect certified by Alibaba Cloud, a project management professional, and research and development of AI products with revenue of hundreds of millions. principal.

file

1. Introduction to Go coroutines

Go coroutine (goroutine) is a concurrent execution unit in the Go language. It is much lighter than traditional threads and is a core component of the Go language concurrency model. In Go, you can run thousands of goroutines simultaneously without worrying about the overhead of regular operating system threads.

What are Go coroutines?

Go coroutines are functions or methods that run in parallel with other functions or methods. You can think of it as similar to lightweight threads. Its main advantage is that its starting and stopping overhead is very small, and it can achieve concurrency more effectively than traditional threads.

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("Hello!")
    }
}

func main() {
    go sayHello() // 启动一个Go协程
    for i := 0; i < 5; i++ {
        time.Sleep(150 * time.Millisecond)
        fmt.Println("Hi!")
    }
}

Output:

Hi!
Hello!
Hi!
Hello!
Hello!
Hi!
Hello!
Hi!
Hello!

Process: In the above code, we have defined a sayHellofunction that prints "Hello!" five times in a loop. In mainthe function, we use gothe keyword to start it sayHelloas a goroutine. After that, we mainprint "Hi!" five more times. Because sayHelloit is a goroutine, it will mainexecute in parallel with the loop in. Therefore, the order in which "Hello!" and "Hi!" are printed in the output may change.

Comparison of Go coroutines and threads

  1. Startup overhead : The startup overhead of Go coroutines is much smaller than that of threads. Therefore, you can easily launch thousands of goroutines.
  2. Memory footprint : The stack size of each Go coroutine starts small (usually in the range of a few KB) and can grow and shrink as needed, whereas threads generally require a fixed, larger stack memory (usually 1MB or more) .
  3. Scheduling : Go coroutines are scheduled by the Go runtime system rather than the operating system. This means that context switching between Go coroutines is less expensive.
  4. Security : Go coroutines provide developers with a simplified concurrency model, combined with synchronization mechanisms such as channels, to reduce common errors in concurrent programs.

Sample code:

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan int) {
    for {
        fmt.Printf("Worker %d received data: %d\n", id, <-ch)
    }
}

func main() {
    ch := make(chan int)

    for i := 0; i < 3; i++ {
        go worker(i, ch) // 启动三个Go协程
    }

    for i := 0; i < 10; i++ {
        ch <- i
        time.Sleep(100 * time.Millisecond)
    }
}

Output:

Worker 0 received data: 0
Worker 1 received data: 1
Worker 2 received data: 2
Worker 0 received data: 3
...

Processing: In this example, we start three worker goroutines to receive data from the same channel. In mainthe function, we send data to the channel. Whenever there is data in the channel, one of the worker goroutines receives it and processes it. Since goroutines run concurrently, it is undefined which goroutine receives the data.

The core advantages of Go coroutines

  1. Lightweight : As mentioned earlier, the startup overhead and memory usage of Go coroutines are much smaller than traditional threads.
  2. Flexible scheduling : Go coroutines are coordinated and scheduled, allowing users to switch tasks at appropriate times.
  3. Simplified concurrency model : Go provides a variety of primitives (such as channels and locks) to make concurrent programming simpler and safer.

Overall, Go coroutines provide developers with an efficient, flexible and safe concurrency model. At the same time, Go's standard library provides a wealth of tools and packages to further simplify the development process of concurrent programs.


2. Basic use of Go coroutines

In Go, coroutines are the basis for building concurrent programs. Creating a coroutine is very simple and gocan be started using keywords. Let's explore some basic usage and examples related to it.

Create and start Go coroutine

To start a Go coroutine simply use gothe keyword followed by a function call. This function can be anonymous or predefined.

Sample code:

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(200 * time.Millisecond)
        fmt.Println(i)
    }
}

func main() {
    go printNumbers()  // 启动一个Go协程
    time.Sleep(1 * time.Second)
    fmt.Println("End of main function")
}

Output:

1
2
3
4
5
End of main function

Process: In this example, we define a printNumbersfunction that will simply print the numbers 1 to 5. In mainthe function, we use gothe keyword to start this function as a new Go coroutine. The main function is executed in parallel with the Go coroutine. To ensure that the main function waits for the Go coroutine to complete execution, we make the main function sleep for 1 second.

Create a Go coroutine using anonymous functions

In addition to starting predefined functions, you can also use anonymous functions to directly start Go coroutines.

Sample code:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("This is a goroutine!")
        time.Sleep(500 * time.Millisecond)
    }()
    fmt.Println("This is the main function!")
    time.Sleep(1 * time.Second)
}

Output:

This is the main function!
This is a goroutine!

Process: In this example, we mainuse an anonymous function directly in the function to create a Go coroutine. In the anonymous function, we simply print a message and let it sleep for 500 milliseconds. The main function first prints its message and then waits for 1 second to ensure that the Go coroutine has enough time to complete execution.

Go coroutine and main function

It is worth noting that if the main function (main) ends, all Go coroutines will be terminated immediately, regardless of their execution status.

Sample code:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        time.Sleep(500 * time.Millisecond)
        fmt.Println("This will not print!")
    }()
}

Processing process: In the above code, the Go coroutine sleeps for 500 milliseconds before printing the message. But since the main function has ended during this period, the Go coroutine is also terminated, so we will not see any output.

In summary, the basic use of Go coroutines is very simple and intuitive, but you need to pay attention to ensure that the main function does not end before all Go coroutines are executed.


3. Synchronization mechanism of Go coroutine

In concurrent programming, synchronization is the key to ensuring that multiple coroutines can share resources or work together effectively and safely. Go provides several primitives to help us achieve this goal.

1. Channels

Channels are the main way in Go to pass data and synchronize execution between coroutines. They provide a mechanism to send data in one coroutine and receive data in another coroutine.

Sample code:

package main

import "fmt"

func sendData(ch chan string) {
    ch <- "Hello from goroutine!"
}

func main() {
    messageChannel := make(chan string)
    go sendData(messageChannel) // 启动一个Go协程发送数据
    message := <-messageChannel
    fmt.Println(message)
}

Output:

Hello from goroutine!

Process: We create a messageChannelchannel named. Then a Go coroutine is started to send sendDatathe string "Hello from goroutine!"to this channel. In the main function we receive this message from the channel and print it.

2. sync.WaitGroup

sync.WaitGroupIs a structure that waits for a group of coroutines to complete. You can increment a count representing the number of coroutines that should be waited for, and decrement the count as each coroutine completes.

Sample code:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers completed.")
}

Output:

Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed.

Processing: We define a workerfunction called which simulates a work task that takes one second to complete. In this function, we use defer wg.Done()to ensure that the count is decremented when the function exits WaitGroup. In mainthe function, we start 5 such worker coroutines, and each time we start one, we use wg.Add(1)to increment the count. wg.Wait()will block until all worker coroutines are notified WaitGroupthat they have completed.

3. Mutex lock( sync.Mutex)

When multiple coroutines need to access shared resources (for example, updating a shared variable), using a mutex lock can ensure that only one coroutine can access the resource at the same time, preventing data races.

Sample code:

package main

import (
    "fmt"
    "sync"
)

var counter int
var lock sync.Mutex

func increment() {
    lock.Lock()
    counter++
    lock.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }

    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

Output:

Final Counter: 1000

Process: We have a global variable counterand we want to increment it concurrently in multiple Go coroutines. In order to ensure that only one Go coroutine can be updated at a time counter, we use a mutex lock lockto synchronize access.

These are some basic methods of Go coroutine synchronization mechanism. Using them correctly can help you write safer and more efficient concurrent programs.


4. Advanced usage of Go coroutines

Advanced usage of Go coroutines involves more complex concurrency patterns, error handling, and coroutine control. We'll explore some common advanced uses and their concrete application examples.

1. selector( select)

selectStatements are the way to handle multiple channels in Go. It allows you to wait for multiple channel operations and perform one of the available operations.

Sample code:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Data from channel 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "Data from channel 2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

Output:

Data from channel 1
Data from channel 2

Process: We create two channels ch1and ch2. Two Go coroutines send data to these two channels respectively, but their sleep times are different. In selectthe statement, we wait for the data to be ready on either of the two channels and then process it. Since ch1its data arrives first, its message is printed first.

2. Timeout processing

Using select, we can easily implement timeout processing for channel operations.

Sample code:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "Data from goroutine"
    }()

    select {
    case data := <-ch:
        fmt.Println(data)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout after 2 seconds")
    }
}

Output:

Timeout after 2 seconds

Processing process: The Go coroutine will sleep for 3 seconds before chsending data. In selectthe statement, we wait for data from this channel or a 2 second timeout. Since the Go coroutine does not send data before timing out, the timeout message is printed.

3. Use contextcoroutine control

contextPackages allow us to share cancellation signals, timeouts, and other settings across multiple coroutines.

Sample code:

package main

import (
    "context"
    "fmt"
    "time"
)

func work(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Received cancel signal, stopping the work")
            return
        default:
            fmt.Println("Still working...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go work(ctx)

    time.Sleep(5 * time.Second)
}

Output:

Still working...
Still working...
Still working...
Received cancel signal, stopping the work

Process: In this example, we create one with a 3 second timeout context. The Go coroutine workwill continue to work until it receives a cancellation signal or times out. After 3 seconds, contextthe timeout is triggered, the Go coroutine receives the cancellation signal and stops working.

These advanced usages provide powerful capabilities for Go coroutines, making complex concurrency patterns and controls possible. Mastering these advanced techniques can help you write more robust and efficient Go concurrent programs.


5. Performance and best practices of Go coroutines

Go coroutines provide lightweight solutions for concurrent programming. But in order to take full advantage of its performance benefits and avoid common pitfalls, it's worth understanding some best practices and performance considerations.

1. Limit the number of concurrencies

Although Go coroutines are lightweight, uncontrolled creation of a large number of Go coroutines may lead to memory exhaustion or increased scheduling overhead.

Sample code:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
}

func main() {
    var wg sync.WaitGroup
    numWorkers := 1000

    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers done")
}

Output:

Worker 1 started
Worker 2 started
...
Worker 1000 started
All workers done

Process: This example creates 1000 worker Go coroutines. Although this number may not cause problems, if more Go coroutines are created without restriction, it may.

2. Avoid race conditions

Multiple Go coroutines may access shared resources at the same time, leading to uncertain results. Use mutexes or other synchronization mechanisms to ensure data consistency.

Sample code:

package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }

    wg.Wait()
    fmt.Println("Final counter value:", counter)
}

Output:

Final counter value: 1000

Process: We use sync.Mutexto ensure exclusive access when incrementing the counter. This ensures data consistency during concurrent access.

3. Use work pool mode

The work pool mode is a method of creating a fixed number of Go coroutines to perform tasks to avoid excessive creation of Go coroutines. Tasks are sent through channels.

Sample code:

package main

import (
    "fmt"
    "sync"
)

func worker(tasks <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for task := range tasks {
        fmt.Printf("Worker processed task %d\n", task)
    }
}

func main() {
    var wg sync.WaitGroup
    tasks := make(chan int, 100)

    // Start 5 workers.
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(tasks, &wg)
    }

    // Send 100 tasks.
    for i := 1; i <= 100; i++ {
        tasks <- i
    }

    close(tasks)
    wg.Wait()
}

Output:

Worker processed task 1
Worker processed task 2
...
Worker processed task 100

Process: We created 5 worker Go coroutines that tasksreceive tasks from the channel. This mode can control the number of concurrency and reuse Go coroutines.

Following these best practices will not only make your Go coroutine code more robust, but also make more efficient use of system resources and improve the overall performance of your program.


6. Summary

With the advancement of computing technology, concurrency and parallelism have become key elements in modern software development. goroutineAs a modern programming language, Go language provides developers with a simple and powerful concurrent programming model through its built-in . But as we've seen in previous chapters, it's crucial to understand how it works, synchronization mechanisms, advanced usage, and performance and best practices.

From this article, we not only learned the basics of Go coroutines and how they work, but also explored some advanced topics on how to maximize their performance. Key insights include:

  1. Lightweight and efficient : Go coroutines are lightweight threads, but their implementation characteristics make them more efficient in a large number of concurrent scenarios.
  2. Synchronization and communication : Go's philosophy is "not to communicate through shared memory, but to share memory through communication." This is reflected in its powerful channelmechanism, which is also key to avoiding many concurrency problems.
  3. Performance and Best Practices : Understanding and following best practices not only ensures the robustness of your code, but can also significantly improve performance.

In the end, while Go provides powerful tools and mechanisms for handling concurrency, the real art lies in using them correctly. As we often see in software engineering, tools are just means, the real power lies in understanding how they work and applying them correctly.

I hope this article has provided you with an in-depth and comprehensive understanding of Go coroutines and provided valuable insights and guidance for your concurrent programming journey. As is often seen in cloud services, Internet service architectures, and other complex systems, true mastery of concurrency is the key to improved performance, scalability, and responsiveness.

Follow the public account [TechLeadCloud] to share full-dimensional knowledge of Internet architecture and cloud service technology. The author has 10+ years of Internet service architecture, AI product development experience, and team management experience. He holds a master's degree from Tongji University in Fudan University, a member of Fudan Robot Intelligence Laboratory, a senior architect certified by Alibaba Cloud, a project management professional, and research and development of AI products with revenue of hundreds of millions. principal.

If it is helpful, please pay more attention to the personal WeChat public account: [TechLeadCloud] to share the full-dimensional knowledge of AI and cloud service research and development, and talk about my unique insights into technology as a TechLead. TeahLead KrisChang, 10+ years of experience in the Internet and artificial intelligence industry, 10+ years of experience in technical and business team management, bachelor's degree in software engineering from Tongji, master's degree in engineering management from Fudan University, Alibaba Cloud certified senior architect of cloud services, AI product business with revenue of hundreds of millions principal.

Microsoft launches new "Windows App" .NET 8 officially GA, the latest LTS version Xiaomi officially announced that Xiaomi Vela is fully open source, and the underlying kernel is NuttX Alibaba Cloud 11.12 The cause of the failure is exposed: Access Key Service (Access Key) exception Vite 5 officially released GitHub report : TypeScript replaces Java and becomes the third most popular language Offering a reward of hundreds of thousands of dollars to rewrite Prettier in Rust Asking the open source author "Is the project still alive?" Very rude and disrespectful Bytedance: Using AI to automatically tune Linux kernel parameter operators Magic operation: disconnect the network in the background, deactivate the broadband account, and force the user to change the optical modem
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/6723965/blog/10111899