0

0

Go Goroutine调度详解:为何无限循环会阻塞其他协程?

聖光之護

聖光之護

发布时间:2025-10-21 08:48:01

|

790人浏览过

|

来源于php中文网

原创

Go Goroutine调度详解:为何无限循环会阻塞其他协程?

一个go goroutine中的无限循环若不主动让出cpu,可能会阻塞其他goroutine的执行,导致程序行为异常。这是因为go的调度器采用协作式调度机制,要求goroutine在特定时机将控制权交还给调度器。本文将深入探讨go goroutine的调度原理,列举常见的让出cpu时机,并提供避免此类阻塞的策略,包括使用`runtime.gosched()`。

Go Goroutine阻塞现象分析

在Go语言中,并发编程的核心是goroutine,它们是轻量级的执行单元,由Go运行时调度器管理。然而,不当的goroutine设计可能导致意想不到的阻塞问题。考虑以下代码示例:

package main

import (
    "fmt"
    "time"
    // "runtime" // 后面会用到
)

func main() {
    timeout := make(chan int)
    go func() {
        time.Sleep(time.Second) // 注意:原始问题中是time.SLeep,这里已修正
        timeout <- 1
    }()

    res := make(chan int)
    go func() {
        for {
            // 这个无限循环不会主动让出CPU
            // runtime.Gosched() // 如果加上这一行,问题会解决
        }
        res <- 1 // 永远不会执行到这里
    }()

    select {
    case <-timeout:
        fmt.Println("timeout")
    case <-res:
        fmt.Println("res")
    }
}

这段代码的预期行为是,在约一秒后,timeout通道接收到值,然后程序打印"timeout"并退出。然而,实际运行时,程序会无限期地运行下去,永远不会打印"timeout"。这是因为第二个goroutine中的for{}无限循环占据了CPU,阻止了调度器将控制权交给第一个goroutine,从而导致time.Sleep(time.Second)无法完成并向timeout通道发送数据。

Go的协作式调度机制

Go语言的调度器采用的是协作式调度(Cooperative Scheduling)模型,这意味着goroutine需要主动或在特定操作下“协作”地将CPU控制权让出给调度器,以便调度器可以将CPU分配给其他等待运行的goroutine。与操作系统层面的抢占式调度(Preemptive Scheduling)不同,协作式调度不会强制中断一个正在运行的goroutine,除非它执行了某些特定操作。

在Go 1.14版本之前,Go的调度器是纯粹的协作式调度。从Go 1.14开始,引入了基于信号的异步抢占式调度,主要用于处理长时间运行的循环,但在某些极端CPU密集型场景下,仍可能出现类似上述的阻塞问题。理解协作式调度的基本原则对于编写健壮的Go并发程序至关重要。

Goroutine让出CPU的常见时机

一个goroutine在以下情况下会主动或被动地将CPU控制权让给调度器:

  1. 无缓冲通道的发送/接收操作 (unbuffered chan send/recv):当goroutine尝试对一个无缓冲通道进行发送或接收操作,而没有其他goroutine准备好匹配的操作时,当前goroutine会阻塞并让出CPU。
  2. 系统调用 (syscalls):包括文件I/O、网络I/O等操作。当goroutine执行系统调用时,它会阻塞并让出CPU,Go调度器可以利用这段时间运行其他goroutine。
  3. 内存分配 (memory allocation):在进行大内存分配时,Go运行时可能会触发调度。
  4. 调用 time.Sleep():显式调用time.Sleep()会使当前goroutine进入休眠状态,并让出CPU。
  5. 显式调用 runtime.Gosched():这是手动让出CPU的最直接方式。

避免Goroutine阻塞的策略

为了避免上述无限循环导致的阻塞问题,我们需要确保CPU密集型goroutine能够周期性地让出CPU。

1. 使用 runtime.Gosched() 手动让出

对于纯粹的CPU密集型循环,如果其中不包含任何I/O、通道操作或time.Sleep等自然让出点,那么在循环内部周期性地调用runtime.Gosched()是确保调度器能够切换到其他goroutine的有效方法。

你好星识
你好星识

你的全能AI工作空间

下载

修改上述阻塞代码,在无限循环中加入runtime.Gosched():

package main

import (
    "fmt"
    "runtime" // 导入runtime包
    "time"
)

func main() {
    timeout := make(chan int)
    go func() {
        time.Sleep(time.Second)
        timeout <- 1
    }()

    res := make(chan int)
    go func() {
        for {
            runtime.Gosched() // 在循环中显式让出CPU
        }
        // res <- 1 // 仍然不会执行到这里,因为循环是无限的
    }()

    select {
    case <-timeout:
        fmt.Println("timeout")
    case <-res:
        fmt.Println("res")
    }
    // 为了看到timeout输出,需要给主goroutine一点时间,
    // 或者在select之后加一个time.Sleep(2 * time.Second)
    time.Sleep(2 * time.Second) // 确保主goroutine不会过早退出
}

现在,运行这段代码,你会发现程序会在大约一秒后打印"timeout",然后继续运行直到time.Sleep(2 * time.Second)结束。runtime.Gosched()确保了无限循环的goroutine不会长时间独占CPU,从而允许time.Sleep的goroutine得以执行。

2. 理解 GOMAXPROCS 的作用与误区

GOMAXPROCS环境变量或runtime.GOMAXPROCS()函数用于设置Go程序可以使用的操作系统线程(P,Processor)的数量。虽然增加GOMAXPROCS的值可以允许Go调度器同时运行更多的goroutine(如果操作系统有足够的CPU核心),但这并不能解决一个goroutine无限循环不让出CPU的问题。

即使将GOMAXPROCS设置为大于1,如果一个goroutine在一个P上陷入了纯粹的CPU密集型无限循环,它仍然会独占该P。更重要的是,Go的垃圾回收器(GC)在执行“Stop The World”(STW)阶段时,需要暂停所有goroutine。如果一个CPU密集型goroutine不让出CPU,GC将无法完成STW,进而无法运行,最终可能导致整个程序因内存耗尽而崩溃,或者GC本身被无限期阻塞。因此,GOMAXPROCS不是解决此类阻塞问题的根本方法。

3. 设计良好的并发程序

最佳实践是设计goroutine时,尽量让它们通过自然的I/O操作、通道通信或定时器等方式周期性地让出CPU。纯粹的CPU密集型无限循环在Go并发编程中应该被视为一个潜在的问题点,需要特别注意。

  • 使用通道进行通信和同步:通道操作是Go中天然的让出点。
  • 利用I/O操作:网络请求、文件读写等都会导致goroutine阻塞并让出CPU。
  • 引入定时器:time.Sleep或time.NewTimer可以用于周期性地暂停goroutine。
  • 避免死循环:确保循环有明确的退出条件或在循环体内部包含让出CPU的机制。

总结

理解Go的协作式调度机制对于编写高效、无阻塞的Go并发程序至关重要。当goroutine执行CPU密集型操作时,必须确保它能够周期性地让出CPU,以便调度器能够公平地分配资源给其他等待运行的goroutine。通过在适当的时机使用runtime.Gosched()或更推荐地通过设计让goroutine自然地执行I/O或通道操作,我们可以有效避免因一个goroutine独占CPU而导致的程序阻塞问题,从而构建出响应迅速且健壮的Go应用程序。

相关专题

更多
线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

480

2023.08.10

Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

233

2023.09.06

go怎么实现链表
go怎么实现链表

go通过定义一个节点结构体、定义一个链表结构体、定义一些方法来操作链表、实现一个方法来删除链表中的一个节点和实现一个方法来打印链表中的所有节点的方法实现链表。

444

2023.09.25

go语言编程软件有哪些
go语言编程软件有哪些

go语言编程软件有Go编译器、Go开发环境、Go包管理器、Go测试框架、Go文档生成器、Go代码质量工具和Go性能分析工具等。本专题为大家提供go语言相关的文章、下载、课程内容,供大家免费下载体验。

246

2023.10.13

0基础如何学go语言
0基础如何学go语言

0基础学习Go语言需要分阶段进行,从基础知识到实践项目,逐步深入。php中文网给大家带来了go语言相关的教程以及文章,欢迎大家前来学习。

693

2023.10.26

Go语言实现运算符重载有哪些方法
Go语言实现运算符重载有哪些方法

Go语言不支持运算符重载,但可以通过一些方法来模拟运算符重载的效果。使用函数重载来模拟运算符重载,可以为不同的类型定义不同的函数,以实现类似运算符重载的效果,通过函数重载,可以为不同的类型实现不同的操作。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

191

2024.02.23

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

228

2024.02.23

go语言开发工具大全
go语言开发工具大全

本专题整合了go语言开发工具大全,想了解更多相关详细内容,请阅读下面的文章。

280

2025.06.11

php与html混编教程大全
php与html混编教程大全

本专题整合了php和html混编相关教程,阅读专题下面的文章了解更多详细内容。

3

2026.01.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 3.7万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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