Skip to content

Go 语言中的 Goroutine ​

2.1 Goroutine 的基本概念 ​

2.1.1 什么是 Goroutine ​

Goroutine 是 Go 语言中的一种轻量级线程。Go 语言通过 Goroutine 实现并发编程,使得多个任务可以并行执行。每个 Go 程序启动时都会有一个默认的 Goroutine,即 main Goroutine。与操作系统的线程不同,Goroutine 由 Go 运行时管理,具有更小的创建和销毁开销。

2.1.2 Goroutine 的声明与启动 ​

启动一个新的 Goroutine 非常简单,只需在函数调用前加上 go 关键字:

go
package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world") // 启动新的 Goroutine
    say("hello")    // 在主 Goroutine 中运行
}

在这个例子中,say("world") 在一个新的 Goroutine 中异步执行,而 say("hello") 则在主 Goroutine 中执行。

2.2 Goroutine 的特性 ​

2.2.1 轻量级 ​

Goroutine 是轻量级的,启动一个新的 Goroutine 只需很少的内存(大约2KB)。这使得 Go 语言能够在单个程序中轻松管理成千上万个并发任务。

2.2.2 独立栈 ​

每个 Goroutine 都有自己的栈空间,栈的大小可以动态增大或减小。初始栈空间很小,可以根据需要自动增长,因此 Goroutine 比传统线程更加高效。Goroutine 的栈最大可以达到 1GB,但通常不会达到这个大小,因为 Go 运行时会在栈空间不够时进行调整。

2.2.3 并发调度 ​

Go 运行时包含一个调度器,用于管理所有的 Goroutine。调度器会将 Goroutine 映射到操作系统的线程上,以便充分利用多核处理器。调度器会公平地分配 CPU 时间给各个 Goroutine,确保它们能够并发执行。

2.2.4 主 Goroutine 退出问题 ​

需要注意的是,如果主 Goroutine 退出,所有其他的 Goroutine 都会被立即终止。因此,在启动其他 Goroutine 时,主 Goroutine 通常需要等待它们完成。例如,可以使用通道(Channel)或者 sync.WaitGroup 来实现这种等待机制。

2.3 Goroutine 与通道的配合 ​

Goroutine 和通道(Channel)是 Go 并发编程的核心。通道用于在 Goroutine 之间传递消息,从而实现同步和通信。

2.3.1 示例:使用通道同步 Goroutine ​

go
package main

import "fmt"

func worker(done chan bool) {
    fmt.Println("Working...")
    done <- true
}

func main() {
    done := make(chan bool, 1)
    go worker(done)
    <-done
    fmt.Println("Done")
}

在这个示例中,主 Goroutine 启动一个新的 Goroutine worker,并等待其通过通道 done 发送完成信号。

2.3.2 示例:使用通道进行任务分配 ​

go
package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d started job %d\n", id, j)
        time.Sleep(time.Second)
        fmt.Printf("Worker %d finished job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= 5; a++ {
        fmt.Println(<-results)
    }
}

在这个示例中,创建了三个工作 Goroutine,主 Goroutine 将任务发送到 jobs 通道,工作 Goroutine 处理任务并将结果发送到 results 通道。

2.4 Goroutine 的应用场景 ​

2.4.1 并行计算 ​

Goroutine 非常适合用于并行计算,例如在多核处理器上进行大量数据处理:

go
package main

import (
    "fmt"
    "sync"
)

func sum(array []int, result chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    sum := 0
    for _, v := range array {
        sum += v
    }
    result <- sum
}

func main() {
    array := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    result := make(chan int, 2)
    var wg sync.WaitGroup

    wg.Add(2)
    go sum(array[:len(array)/2], result, &wg)
    go sum(array[len(array)/2:], result, &wg)
    wg.Wait()
    close(result)

    total := 0
    for r := range result {
        total += r
    }
    fmt.Println("Total:", total)
}

2.4.2 网络服务 ​

Goroutine 也广泛用于网络服务器的实现中,每个请求可以在一个单独的 Goroutine 中处理,从而提高并发性能:

go
package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

在这个示例中,每个 HTTP 请求都会启动一个新的 Goroutine 来处理。

2.5 Goroutine 与线程的对比 ​

2.5.1 轻量级与高效 ​

Goroutine 相比传统的线程更加轻量级和高效。Goroutine 的栈空间初始很小,并且可以根据需要动态增长。创建和销毁 Goroutine 的开销也比线程更小,这使得在 Go 中可以轻松管理大量的并发任务。

2.5.2 调度机制 ​

Go 运行时包含一个调度器,用于管理所有的 Goroutine。调度器将 Goroutine 映射到操作系统线程上,以便充分利用多核处理器。相比之下,线程由操作系统管理,调度开销较大。

2.5.3 通信与同步 ​

Goroutine 通常通过通道进行通信和同步,这种方式既简洁又安全,避免了使用共享内存带来的复杂性和潜在的竞争条件。而线程之间的通信和同步通常需要使用锁和条件变量等机制,代码复杂且容易出错。

2.6 Goroutine 与其他语言的协程对比 ​

2.6.1 Python ​

Python 的协程通过 async 和 await 关键字实现,并且需要与事件循环(如 asyncio)配合使用。Python 的协程主要用于 I/O 密集型任务,不能自动并发执行 CPU 密集型任务。

2.6.2 Lua ​

Lua 的协程是通过 coroutine 库实现的,属于用户级的协程。Lua 协程需要手动进行切换,不像 Goroutine 那样由运行时自动管理。

2.6.3 C++ ​

C++20 引入了协程,通过 co_await、co_yield 和 co_return 等关键字实现。C++ 的协程与 Go 的 Goroutine 类似,都是轻量级的并发单元,但 C++ 的协程需要开发者自己处理调度和资源管理。

2.6.4 C# ​

C# 的协程通过 async 和 await 关键字实现,主要用于异步 I/O 操作。C# 的协程需要与任务(Task)和任务调度器配合使用。

2.6.5 Kotlin ​

Kotlin 的协程通过 suspend 关键字和 CoroutineScope 实现,支持结构化并发和上下文切换。Kotlin 的协程与 Go 的 Goroutine 相似,都是由运行时自动调度和管理。

2.7 Goroutine 与线程的绑定 ​

Go 提供了 runtime.LockOSThread 和 runtime.UnlockOSThread 函数,用于将 Goroutine 绑定到特定的操作系统线程上。通常在需要与特定线程关联的资源(如 GUI 线程)时使用:

go
package main

import (
    "fmt"
    "runtime"
)

func

 main() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    go func() {
        fmt.Println("This Goroutine is running on a specific thread.")
    }()
}

2.8 Goroutine 池的实现 ​

在某些情况下,控制 Goroutine 的数量是必要的,例如为了限制资源使用或防止 Goroutine 过多导致的调度开销。可以使用 Goroutine 池来管理 Goroutine 的数量。

2.8.1 示例:简单的 Goroutine 池 ​

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for j := range jobs {
        fmt.Printf("Worker %d started job %d\n", id, j)
        fmt.Printf("Worker %d finished job %d\n", id, j)
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    var wg sync.WaitGroup

    const numWorkers = 3
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, &wg)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

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

在这个示例中,创建了一个包含三个工作 Goroutine 的 Goroutine 池,并分配了五个任务。工作 Goroutine 从 jobs 通道读取任务并处理。

2.8.2 Goroutine 的退出与让渡调度 ​

在某些情况下,可能需要手动控制 Goroutine 的退出和调度。可以使用 runtime.Goexit 退出当前 Goroutine,使用 runtime.Gosched 让当前 Goroutine 让出 CPU 时间片:

go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func worker() {
    for i := 0; i < 3; i++ {
        fmt.Println("Working...")
        runtime.Gosched() // 让出 CPU 时间片
    }
    fmt.Println("Worker done.")
}

func main() {
    go worker()
    time.Sleep(100 * time.Millisecond) // 等待 worker 完成
    fmt.Println("Main done.")
}

在这个示例中,worker Goroutine 每次循环都让出 CPU 时间片,主 Goroutine 等待 worker 完成后退出。

通过以上内容,我们详细介绍了 Goroutine 的基本概念、特性、与通道的配合使用、应用场景、与线程及其他语言协程的对比、线程绑定、Goroutine 池的实现,以及 Goroutine 的退出和调度控制。这些知识将帮助读者更好地理解和使用 Goroutine 进行高效的并发编程。

Hello Golang