0

0

Go并发编程:select与default陷阱及调度器行为分析

碧海醫心

碧海醫心

发布时间:2025-09-18 12:59:59

|

683人浏览过

|

来源于php中文网

原创

Go并发编程:select与default陷阱及调度器行为分析

本文深入探讨了Go语言中select语句与default子句结合使用时可能导致的并发问题,特别是当default子句形成忙等待循环时,可能饿死其他goroutine,导致程序无法正常终止。通过分析一个具体的爬虫示例,文章揭示了fmt.Print等I/O操作如何无意中成为调度器让出CPU的契机,并提供了一种避免此类忙等待的正确解决方案,强调了理解Go调度器行为的重要性。

Go并发爬虫中的select与default行为分析

go语言中,select语句是实现并发模式的核心机制之一,它允许goroutine等待多个通信操作。然而,当select语句包含default子句时,其行为会变得非阻塞,这在某些情况下可能引入不易察觉的并发问题。本文将通过一个go语言爬虫示例,详细剖析select与default子句在特定场景下的交互,以及它如何影响go调度器的行为。

问题场景复现

我们以一个简单的Go语言网页爬虫为例,该爬虫使用goroutine并发抓取网页,并通过通道(channel)进行任务调度和完成信号的传递。核心的爬虫逻辑Crawl函数如下所示:

package main

import (
    "fmt"
    "os"
    "time" // Added for demonstration of busy-waiting
)

type Fetcher interface {
    Fetch(url string) (body string, urls []string, err error)
}

func crawl(todo Todo, fetcher Fetcher,
    todoList chan Todo, done chan bool) {
    body, urls, err := fetcher.Fetch(todo.url)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Printf("found: %s %q\n", todo.url, body)
        for _, u := range urls {
            todoList <- Todo{u, todo.depth - 1}
        }
    }
    done <- true // 发送完成信号
    return
}

type Todo struct {
    url   string
    depth int
}

func Crawl(url string, depth int, fetcher Fetcher) {
    visited := make(map[string]bool)
    doneCrawling := make(chan bool, 100) // 缓冲通道,用于接收爬取完成信号
    toDoList := make(chan Todo, 100)     // 缓冲通道,用于发送待爬取任务
    toDoList <- Todo{url, depth}         // 初始任务

    crawling := 0 // 正在进行的爬取任务计数器
    for {
        select {
        case todo := <-toDoList: // 接收待爬取任务
            if todo.depth > 0 && !visited[todo.url] {
                crawling++
                visited[todo.url] = true
                go crawl(todo, fetcher, toDoList, doneCrawling)
            }
        case <-doneCrawling: // 接收爬取完成信号
            crawling--
        default: // 无其他通道操作时执行
            if os.Args[1] == "ok" {
                fmt.Print("") // 关键差异点
            }
            if crawling == 0 { // 所有任务完成
                goto END
            }
            // time.Sleep(time.Millisecond) // 可用于缓解忙等待,但不是根本解决方案
        }
    }
END:
    return
}

func main() {
    // 模拟的Fetcher实现
    var fetcher = &fakeFetcher{
        "http://golang.org/": &fakeResult{
            "The Go Programming Language",
            []string{"http://golang.org/pkg/", "http://golang.org/cmd/"},
        },
        "http://golang.org/pkg/": &fakeResult{
            "Packages",
            []string{"http://golang.org/", "http://golang.org/cmd/", "http://golang.org/pkg/fmt/", "http://golang.org/pkg/os/"},
        },
        "http://golang.org/pkg/fmt/": &fakeResult{
            "Package fmt",
            []string{"http://golang.org/", "http://golang.org/pkg/"},
        },
        "http://golang.org/pkg/os/": &fakeResult{
            "Package os",
            []string{"http://golang.org/", "http://golang.org/pkg/"},
        },
    }
    Crawl("http://golang.org/", 4, fetcher)
    fmt.Println("Crawling finished.")
}

type fakeFetcher map[string]*fakeResult
type fakeResult struct {
    body string
    urls []string
}

func (f *fakeFetcher) Fetch(url string) (string, []string, error) {
    if res, ok := (*f)[url]; ok {
        return res.body, res.urls, nil
    }
    return "", nil, fmt.Errorf("not found: %s", url)
}

当我们使用go run your_program.go ok运行上述代码时,程序能够正常终止。然而,如果使用go run your_program.go nogood运行,程序将无限期地挂起,无法终止。唯一的区别在于select语句的default子句中是否包含fmt.Print("")。

根源分析:select与Go调度器

问题的核心在于select语句与default子句的交互方式,以及Go调度器的行为。

  1. select与default的非阻塞特性: 当select语句包含default子句时,它会变为非阻塞模式。这意味着如果没有任何通道操作(发送或接收)准备就绪,select不会阻塞等待,而是立即执行default子句中的代码。在上述示例中,toDoList和doneCrawling通道在某些时刻可能没有可用的数据或空间,此时default子句就会被频繁执行。

  2. 忙等待(Busy-Waiting)与调度器饥饿: 在nogood场景下,default子句中没有fmt.Print("")。当toDoList和doneCrawling通道暂时没有活动时,主Crawl goroutine会以极快的速度反复执行default子句中的if crawling == 0 { goto END }检查。这是一个典型的忙等待循环,它会持续占用CPU,导致Go调度器无法有效地将CPU时间分配给其他重要的goroutine,尤其是那些负责实际爬取任务(crawl函数)并向toDoList和doneCrawling发送数据的goroutine。这些crawl goroutine因此被“饿死”,无法及时将任务或完成信号发送到通道,从而使得主Crawl goroutine的select语句永远无法从通道接收到数据,陷入无限的忙等待。

  3. fmt.Print("")的意外作用:fmt.Print函数涉及底层I/O操作(即使是打印空字符串)。在Go语言中,涉及系统调用的操作(如I/O)是调度器显式的让出点(yield point)。当fmt.Print("")被执行时,当前goroutine会暂停执行,等待I/O操作完成,这为Go调度器提供了机会去运行其他处于就绪状态的goroutine。在这种情况下,被饿死的crawl goroutine得以执行,它们能够将数据发送到toDoList和doneCrawling通道,从而打破主Crawl goroutine的忙等待状态,使其能够接收到数据并最终正常终止。

    另一个佐证是,如果设置GOMAXPROCS=2(即允许Go程序使用两个操作系统线程),程序在nogood模式下也能正常运行。这是因为有了更多的操作系统线程,即使一个线程陷入忙等待,另一个线程仍有能力调度并执行其他goroutine,从而缓解了调度器饥饿问题。

正确的解决方案

为了避免这种忙等待和调度器饥饿问题,我们应该重新设计select语句的结构,确保在没有通道活动时,主goroutine能够适当地阻塞或让出CPU。最直接且推荐的解决方案是将终止条件检查逻辑移到select语句之外,或者确保default子句中包含明确的让出机制(例如runtime.Gosched()或time.Sleep(),但这通常不是最佳实践)。

sematic
sematic

一个开源的机器学习平台

下载

以下是改进后的Crawl函数中的for循环:

func Crawl(url string, depth int, fetcher Fetcher) {
    visited := make(map[string]bool)
    doneCrawling := make(chan bool, 100)
    toDoList := make(chan Todo, 100)
    toDoList <- Todo{url, depth}

    crawling := 0
    for {
        select {
        case todo := <-toDoList:
            if todo.depth > 0 && !visited[todo.url] {
                crawling++
                visited[todo.url] = true
                go crawl(todo, fetcher, toDoList, doneCrawling)
            }
        case <-doneCrawling:
            crawling--
        }
        // 将终止条件检查移到select外部
        if crawling == 0 {
            break // 退出循环
        }
    }
    fmt.Println("所有爬取任务已完成。") // 确认退出
    return
}

在这个改进后的代码中:

  1. select语句不再包含default子句。这意味着如果toDoList和doneCrawling通道都没有准备好,主Crawl goroutine会阻塞,直到其中一个通道有活动。
  2. crawling == 0的终止条件检查被移到了select语句的外部。这样,只有当select语句完成了一次通道操作(无论是接收任务还是接收完成信号)之后,才会检查是否所有任务都已完成。如果crawling计数器归零,说明所有子goroutine都已完成并发送了完成信号,此时主goroutine可以安全地退出循环。

这种结构确保了主goroutine不会陷入忙等待,而是高效地利用Go调度器的阻塞机制,只有在有实际工作可做时才被唤醒。

并发编程最佳实践

  1. 谨慎使用select的default子句: default子句将select变为非阻塞模式。如果不需要非阻塞行为,应避免使用default。如果确实需要非阻塞检查,请确保default子句中的逻辑不会导致忙等待,例如,可以加入一个短时间的time.Sleep或runtime.Gosched()来显式让出CPU,但更好的做法是重新考虑程序设计,避免频繁的空转。
  2. 理解Go调度器: Go调度器是协作式的,它会在某些点(如系统调用、通道操作、垃圾回收等)让出CPU。了解这些让出点有助于理解并发程序的行为。
  3. 正确管理并发任务的生命周期: 对于需要等待所有并发任务完成的场景,sync.WaitGroup通常是比手动管理计数器和通道更简洁、更健壮的方案。例如,可以使用WaitGroup来等待所有crawl goroutine的完成。
  4. 避免全局状态和竞态条件: 在并发编程中,对共享状态的访问需要通过互斥锁(sync.Mutex)或通道进行同步,以避免数据竞态。本例中的visited map就是一个共享状态,通过在主goroutine中集中管理,避免了竞态。

通过对这个案例的深入分析,我们不仅解决了特定的程序挂起问题,更重要的是,加深了对Go语言中select语句、default子句以及Go调度器行为的理解,这对于编写高效、健壮的并发程序至关重要。

相关文章

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

183

2023.09.27

if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

711

2023.08.22

go语言goto的用法
go语言goto的用法

本专题整合了go语言goto的用法,阅读专题下面的文章了解更多详细内容。

129

2025.09.05

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

248

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

205

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1435

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

609

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

547

2024.03.22

vlookup函数使用大全
vlookup函数使用大全

本专题整合了vlookup函数相关 教程,阅读专题下面的文章了解更多详细内容。

28

2025.12.30

热门下载

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

精品课程

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

共32课时 | 3.1万人学习

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

共10课时 | 0.8万人学习

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

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