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


深入理解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
  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执行
  11. main Goroutine完成,程序退出。

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

AI建筑知识问答 AI建筑知识问答

用人工智能ChatGPT帮你解答所有建筑问题

AI建筑知识问答 172 查看详情 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已完成并发送信号。")
}

通过两次

更推荐的做法是使用 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 通常是比多个
  • 资源清理: 当主Goroutine退出时,所有子Goroutine都会被强制终止。在某些情况下,如果子Goroutine正在执行关键的清理操作,这可能会导致问题。因此,确保在主Goroutine退出前,所有必要的清理工作都已完成。

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

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


# 到第  # 青海省网站建设800元  # 石湾丹灶网站建设  # 桂林房地产营销策划推广  # 设计品牌营销推广  # 毕节seo营销公司有哪些  # 科技服务公司营销推广  # 网站seo推广渠道  # 深圳网站建设首选  # 飞翔时代网站建设  # 湖州营销推广加盟电话号码  # 它会  # 发送到  # 多个  # go  # 器中  # 知识问答  # 布尔  # 能在  # 第一个  # 为什么  # 同步机制  # 并发编程  # ai  # 工具  # go语言  # 操作系统 


相关栏目: 【 Google疑问12 】 【 Facebook疑问10 】 【 优化推广96088 】 【 技术知识133117 】 【 IDC资讯59369 】 【 网络运营7196 】 【 IT资讯61894


相关推荐: 英雄联盟争者留名活动介绍  Google Drive API服务器端访问指南:服务账户认证详解  在J*a中如何实现类的继承与方法重用_OOP继承方法重用技巧分享  如何自定义苹果手机铃声  大熊猫抓取竹子的“大拇指”其实是什么?蚂蚁庄园课堂今天答案最新11月30日  Composer reinstall命令重装损坏的包  《下一站江湖2》心法融合技巧  VS Code的时间线(Timeline)视图:您的代码时光机  鼠标没反应了怎么办 无线/有线鼠标失灵的解决方法【详解】  《米姆米姆哈》米姆获取及技能攻略  漫蛙manwa官网浏览入口_漫蛙漫画网页版访问链接  京东快递物流信息不更新怎么办_物流停滞原因与处理方法  飞飞漫画漫画阅读官网_飞飞漫画漫画阅读官网进入阅读  漫蛙app官方版手机正版入口-漫蛙漫画manwa在线漫画正版入口  TikTok搜索结果不显示怎么办 TikTok搜索刷新与优化方法  鲁班大师乓乓皮肤获取方法  TikTok网页版实时观看入口 TikTok网页版短视频在线浏览  发布小红书怎么屏蔽粉丝?屏蔽粉丝能看到吗?  《飞猪旅行》购买汽车票方法  铁拳8在线玩 铁拳8在线秒玩入口  免费占卜在线神算_免费占卜手机神算  word表格如何按某一列内容进行排序_Word表格按列排序方法  泰拉瑞亚水晶无法放置问题  iPhone16Plus参数配置如何调整声音_iPhone16Plus参数配置声音调整详细方法  使用CSS :has() 选择器实现父元素样式控制:从子元素反向应用样式  抖音号怎么解除企业认证改成个人?改成个人有影响吗?  VS Code快捷键when上下文子句的妙用  J*aScript 数值去小数位处理:多种方法与实践  TikTok私信无法发送表情怎么办 TikTok消息表情发送修复方法  《漫蛙manwa2》防走失网页版链接2025  Excel宏怎么删除_Excel中删除宏的详细操作流程  Flask 应用中图片动态更新与上传:实现客户端定时刷新与服务器端文件管理  Go Goroutine调度与并发执行深度解析  谷歌浏览器官方镜像获取方法_谷歌浏览器网页版入口极速直达  Lar*el怎么实现全文搜索_Lar*el Scout集成Algolia教程  Excel如何快速找到并断开外部数据源链接_Excel外部数据源断开方法  邦丰播放器频道搜索设置  CSS动画如何实现图标旋转并放大_transform rotate scale @keyframes实现  百度竞价WAP显示PC链接问题  三星A55应用闪退排查步骤_Samsung A55稳定性优化技巧  抄漫画官网防走失地址_抄漫画最新漫画完整版阅读入口  VBA Outlook邮件自动化:高效集成Excel数据与列标题的策略  精通VS Code多光标编辑以实现闪电般快速的修改  C#中的Record类型有什么优势?C# 9新特性Record与Class的用法区别  顺丰快递收费标准查询_如何查看顺丰最新收费价格  告别阻塞等待:如何使用GuzzlePromises优雅处理PHP异步操作,提升应用响应速度  获取WooCommerce产品在后台编辑页面的分类ID  win11怎么更改账户类型 Win11标准用户和管理员权限切换【教程】  《猎聘》筛选猎头岗位方法  电脑从睡眠中被自动唤醒怎么办_Windows唤醒源事件查看与禁用【解决】 

 2025-10-25

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.