首页 > 后端开发 > Golang > 正文

深入理解Go并发:Goroutines、Channels与调度器行为

碧海醫心
发布: 2025-10-25 11:34:44
原创
428人浏览过

深入理解go并发:goroutines、channels与调度器行为

本文旨在深入探讨Go语言的并发模型,重点解析Goroutines、Channels的工作原理及其与Go调度器之间的关系。通过分析一个具体的并发示例,我们将揭示Go程序执行顺序的非确定性,并提供如何使用Channels进行有效同步和通信的策略,以确保程序行为符合预期。

Go语言以其内置的并发原语而闻名,这些原语使得编写并发程序变得简单而高效。核心概念包括Goroutines(轻量级线程)和Channels(用于Goroutines之间通信的管道)。然而,初学者在理解Go调度器如何管理这些并发任务时,常会遇到一些困惑,尤其是在涉及执行顺序和同步时。

Goroutines与Channels基础

在Go中,Goroutine是由Go运行时管理的轻量级执行单元。它们比传统操作系统线程开销更小,可以轻松启动数千甚至数百万个。Channels是Goroutines之间进行通信和同步的主要方式。它们允许Goroutines安全地发送和接收数据,从而避免了传统并发编程中常见的竞态条件。

一个典型的Go并发程序会创建多个Goroutines,并通过Channels协调它们的执行。例如,以下代码片段展示了两个Goroutines (display 和 sum),它们各自执行一些任务,并通过一个布尔型Channel (c) 发送完成信号。主Goroutine (main) 则等待从这个Channel接收信号。

package main

import (
    "fmt"
    "time" // 引入time包用于模拟耗时操作
)

// display Goroutine打印一条消息并发送完成信号
func display(msg string, c chan bool) {
    fmt.Println("display first message:", msg)
    c <- true // 发送完成信号
}

// sum Goroutine执行一个长时间的计算并发送完成信号
func sum(c chan bool) {
    sumVal := 0
    // 模拟一个非常耗时的计算
    for i := 0; i < 10000000000; i++ { 
        sumVal++
    }
    fmt.Println(sumVal)
    c <- true // 发送完成信号
}

func main() {
    c := make(chan bool) // 创建一个无缓冲的布尔型Channel

    go display("hello", c) // 启动display Goroutine
    go sum(c)              // 启动sum Goroutine

    <-c // 主Goroutine等待从Channel c接收一个信号
    // 程序在接收到第一个信号后可能会退出
}
登录后复制

在上述代码中,预期的输出可能会让初学者感到困惑。根据Go调度器的行为,程序的实际输出可能是:

display first message: hello
10000000000
登录后复制

而不是某些人可能预期的只打印一行 display first message: hello 就退出。

Go调度器与非确定性执行

理解上述输出的关键在于Go调度器的工作方式。Go调度器是一个非确定性的组件,它负责在可用的操作系统线程上调度Goroutines的执行。它会根据内部算法在不同的Goroutines之间进行切换(抢占式调度),以实现并发执行的效果。这意味着Goroutines的精确执行顺序是无法保证的,并且可能在每次运行程序时有所不同。

对于上述示例,一个可能的执行序列如下:

  1. main Goroutine启动,创建并启动 display 和 sum 两个新的Goroutine。
  2. Go调度器选择运行 display Goroutine。
  3. display Goroutine执行 fmt.Println("display first message: hello") 并打印消息。
  4. display Goroutine尝试执行 c <- true。由于 c 是一个无缓冲Channel,并且此时 main Goroutine还没有准备好接收数据,display Goroutine会被阻塞。
  5. Go调度器此时会切换到另一个可运行的Goroutine,例如 sum Goroutine。
  6. sum Goroutine开始执行其耗时的循环计算。由于循环次数非常大,sum 会占用相当长的时间。
  7. 在 sum Goroutine执行期间,Go调度器可能会在不同的时间点进行切换。它可能在 sum 完成其计算并打印 10000000000 之后,再次切换回 display Goroutine。
  8. 一旦 display Goroutine被重新调度,它会成功地将 true 发送到Channel c(因为此时 main Goroutine可能已被调度并准备好接收,或者调度器选择先让发送完成)。
  9. Go调度器随后可能调度 main Goroutine。
  10. main Goroutine执行 <-c,从Channel c 接收到 true。
  11. main Goroutine完成,程序退出。

这个序列解释了为什么两个Goroutine的打印输出都可能在 main Goroutine退出之前出现。关键在于 main 函数中的 <-c 只等待并接收 一个 值。由于 display 和 sum 都尝试向 c 发送值,main 只需要等待其中 任意一个 Goroutine成功发送即可。在上述场景中,尽管 sum 花了很长时间,但它和 display 的打印操作都可能在 main 接收到第一个信号之前完成。

钉钉 AI 助理
钉钉 AI 助理

钉钉AI助理汇集了钉钉AI产品能力,帮助企业迈入智能新时代。

钉钉 AI 助理21
查看详情 钉钉 AI 助理

控制Goroutine的执行与同步

如果需要确保特定的执行顺序或等待所有Goroutines完成,我们需要更精细的同步机制

1. 等待所有Goroutines完成

如果 main Goroutine需要等待所有启动的Goroutines都完成它们的任务并发送信号,那么它需要从Channel中接收相应数量的信号。

package main

import (
    "fmt"
    "time"
)

func display(msg string, c chan bool) {
    fmt.Println("display first message:", msg)
    time.Sleep(100 * time.Millisecond) // 模拟一些工作
    c <- true
}

func sum(c chan bool) {
    sumVal := 0
    for i := 0; i < 1000000000; i++ { // 缩短循环以方便演示
        sumVal++
    }
    fmt.Println(sumVal)
    c <- true
}

func main() {
    c := make(chan bool)

    go display("hello", c)
    go sum(c)

    // 等待两个Goroutine都发送完成信号
    <-c // 等待display或sum中的一个
    <-c // 等待另一个
    fmt.Println("所有Goroutine已完成并发送信号。")
}
登录后复制

通过两次 <-c 操作,main Goroutine会阻塞直到从Channel c 接收到两个值,从而确保 display 和 sum 都已执行到发送信号的步骤。

更推荐的做法是使用 sync.WaitGroup 来等待一组Goroutines完成:

package main

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

func displayWithWG(msg string, wg *sync.WaitGroup) {
    defer wg.Done() // Goroutine完成后调用Done
    fmt.Println("display first message:", msg)
    time.Sleep(100 * time.Millisecond)
}

func sumWithWG(wg *sync.WaitGroup) {
    defer wg.Done() // Goroutine完成后调用Done
    sumVal := 0
    for i := 0; i < 1000000000; i++ {
        sumVal++
    }
    fmt.Println(sumVal)
}

func main() {
    var wg sync.WaitGroup

    wg.Add(2) // 设置需要等待的Goroutine数量

    go displayWithWG("hello", &wg)
    go sumWithWG(&wg)

    wg.Wait() // 阻塞直到所有Goroutine都调用了Done
    fmt.Println("所有Goroutine已完成。")
}
登录后复制

2. 获取第一个完成的Goroutine结果并退出

如果目标是只获取第一个完成的Goroutine的结果,并立即退出程序,那么Channel应该被设计为携带实际的结果,并且主Goroutine只接收一个结果。

package main

import (
    "fmt"
    "time"
)

// displayResult Goroutine发送其结果到Channel
func displayResult(msg string, resultChan chan string) {
    time.Sleep(50 * time.Millisecond) // 模拟较快完成
    resultChan <- "Display Goroutine: " + msg
}

// sumResult Goroutine发送其结果到Channel
func sumResult(resultChan chan string) {
    sumVal := 0
    for i := 0; i < 1000000000; i++ { // 模拟较慢完成
        sumVal++
    }
    resultChan <- fmt.Sprintf("Sum Goroutine: %d", sumVal)
}

func main() {
    resultChan := make(chan string) // 创建一个用于传递结果的Channel

    go displayResult("hello", resultChan)
    go sumResult(resultChan)

    // 主Goroutine等待并打印第一个收到的结果
    fmt.Println("第一个完成的任务结果:", <-resultChan)
    // 程序在接收到第一个结果后立即退出。
    // 另一个Goroutine可能仍在运行,但其结果不会被处理。
}
登录后复制

在这个例子中,main Goroutine只从 resultChan 接收一个字符串值。无论 displayResult 或 sumResult 哪个先将结果发送到 resultChan,main 都会接收到它,打印,然后程序退出。这有效地实现了“谁先完成,就用谁的结果”的模式。

注意事项与总结

  • 非确定性是常态: 除非通过Channel或其他同步机制明确指定,否则不要对Goroutines的执行顺序做任何假设。
  • Channel的角色: Channel不仅用于数据传输,更重要的是用于Goroutines之间的同步。无缓冲Channel在发送和接收操作时会阻塞,直到另一端就绪,这使得它们成为强大的同步工具
  • 选择合适的同步机制: 对于等待一组Goroutines完成,sync.WaitGroup 通常是比多个 <-chan 操作更清晰、更推荐的选择。对于Goroutines之间的数据交换和更复杂的协调,Channels是首选。
  • 资源清理: 当主Goroutine退出时,所有子Goroutine都会被强制终止。在某些情况下,如果子Goroutine正在执行关键的清理操作,这可能会导致问题。因此,确保在主Goroutine退出前,所有必要的清理工作都已完成。

理解Go调度器如何管理Goroutines以及如何有效地使用Channels是编写健壮、高效Go并发程序的基石。通过实践和细致的思考,可以避免常见的并发陷阱,并充分利用Go语言的并发优势。

以上就是深入理解Go并发:Goroutines、Channels与调度器行为的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号