0

0

深入理解Go语言中select{}的行为与高效并发模式

碧海醫心

碧海醫心

发布时间:2025-09-01 14:32:18

|

871人浏览过

|

来源于php中文网

原创

深入理解go语言中select{}的行为与高效并发模式

本文旨在解析Go语言中select{}语句在没有通道操作时为何不会“永远阻塞”以等待其他goroutine完成,并分析由此导致的死锁问题。我们将探讨如何利用sync.WaitGroup或更通用的工作池模式来正确管理并发任务的生命周期,确保主goroutine能够优雅地等待所有子goroutine执行完毕,从而避免死锁并构建健壮的并发程序。

Go并发中的select{}行为解析

在Go语言中,select语句主要用于监听多个通道操作(发送或接收)。当select语句内部包含多个case时,它会阻塞直到其中一个case的通道操作准备就绪。如果所有case都不就绪,且存在default分支,则select会立即执行default分支。

然而,当select语句中不包含任何case,即select{}时,它的行为是简单地阻塞当前执行的goroutine,使其进入休眠状态。它不会自动监测或等待其他正在运行的goroutine完成。在原始代码示例中:

func main() {
    // ... 前面部分代码 ...
    for _, f := range files {
        activeWorkers <- true
        fmt.Printf("activeWorkers is %d long.\n", len(activeWorkers))
        go runTask(f, &activeWorkers)
    }
    select{} // 问题所在:主goroutine在此处阻塞
}

main goroutine在完成for循环后,会启动所有的runTask goroutine,然后进入select{}语句。此时,main goroutine会立即阻塞。runTask goroutine会独立运行,并在完成任务后执行

问题在于,select{}并不知道其他runTask goroutine的存在或其完成状态。当所有runTask goroutine都执行完毕并退出后,main goroutine仍然在select{}中休眠。此时,Go运行时检测到程序中所有的goroutine(包括main和所有已完成的runTask)都处于休眠状态,没有任何goroutine可以继续执行或唤醒其他goroutine,从而判定为死锁(all goroutines are asleep - deadlock!)。

立即学习go语言免费学习笔记(深入)”;

因此,select{}并非“永远阻塞”以等待其他goroutine,它只是阻塞了当前goroutine。要正确等待其他goroutine完成,我们需要更明确的同步机制

使用通道作为信号量并正确等待任务

原始代码的意图是使用带缓冲的通道activeWorkers作为信号量,限制同时运行的runTask goroutine数量。这种模式是有效的,但关键在于main goroutine需要一种方式来知道所有runTask goroutine何时完成。sync.WaitGroup是解决此类问题的标准Go同步原语。

sync.WaitGroup提供了一个计数器,可以用于等待一组goroutine完成。

  • Add(delta int):将计数器增加delta。
  • Done():将计数器减一(通常在defer语句中调用)。
  • Wait():阻塞直到计数器归零。

下面是使用sync.WaitGroup修正后的原始代码,它保留了使用通道作为信号量的逻辑,并解决了死锁问题:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// runTask 模拟一个需要处理时间的任务
func runTask(t string, ch chan bool, wg *sync.WaitGroup) {
    defer wg.Done() // 确保任务完成后递减 WaitGroup 计数器
    start := time.Now()
    fmt.Println("starting task", t)
    time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // 模拟处理时间
    fmt.Println("done running task", t, "in", time.Since(start))
    <-ch // 任务完成后,从信号量通道中取出一个值,释放一个槽位
}

func main() {
    numWorkers := 3 // 最大并发任务数
    files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}

    activeWorkers := make(chan bool, numWorkers) // 用作并发控制的信号量通道
    var wg sync.WaitGroup                        // 用于等待所有任务完成

    fmt.Println("Starting task scheduling...")
    for _, f := range files {
        activeWorkers <- true // 尝试向通道发送值,如果通道已满,则阻塞,直到有槽位释放
        wg.Add(1)             // 为每个启动的任务增加 WaitGroup 计数器
        fmt.Printf("activeWorkers 队列长度: %d. 启动任务: %s\n", len(activeWorkers), f)
        go runTask(f, activeWorkers, &wg) // 启动任务 goroutine
    }

    wg.Wait() // 阻塞主 goroutine,直到所有任务都调用了 wg.Done()
    fmt.Println("所有任务已完成。程序即将退出。")
}

在这个修正后的版本中:

  1. main goroutine在启动每个runTask之前调用wg.Add(1)。
  2. runTask goroutine在defer wg.Done()中确保任务结束时(无论是正常完成还是发生panic)都会递减WaitGroup计数器。
  3. main goroutine在for循环结束后,调用wg.Wait()。这会阻塞main goroutine,直到WaitGroup的计数器归零,即所有runTask goroutine都已完成。

这样,主goroutine会一直等待,直到所有并发任务都安全结束,从而避免了死锁。

迅易年度企业管理系统开源完整版
迅易年度企业管理系统开源完整版

系统功能强大、操作便捷并具有高度延续开发的内容与知识管理系统,并可集合系统强大的新闻、产品、下载、人才、留言、搜索引擎优化、等功能模块,为企业部门提供一个简单、易用、开放、可扩展的企业信息门户平台或电子商务运行平台。开发人员为脆弱页面专门设计了防刷新系统,自动阻止恶意访问和攻击;安全检查应用于每一处代码中,每个提交到系统查询语句中的变量都经过过滤,可自动屏蔽恶意攻击代码,从而全面防止SQL注入攻击

下载

更通用的并发模式:工作池(Worker Pool)

虽然使用sync.WaitGroup结合带缓冲通道可以解决上述问题,但对于更复杂的任务调度和结果收集场景,Go语言中更推荐使用工作池(Worker Pool)模式。工作池模式将任务提交和任务执行解耦,并提供了一种优雅的方式来控制并发度、收集结果。

工作池通常包含以下组件:

  • 任务输入通道 (Input Channel):用于提交待处理的任务。
  • 工作协程 (Worker Goroutines):一组固定数量的goroutine,它们从输入通道接收任务并执行。
  • 结果输出通道 (Output Channel) (可选):用于收集工作协程处理后的结果。

以下是工作池模式的示例代码:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

// runTask 模拟一个需要处理时间的任务,并返回任务标识
func runTask(t string) string {
    start := time.Now()
    fmt.Println("starting task", t)
    time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // 模拟处理时间
    fmt.Println("done running task", t, "in", time.Since(start))
    return t // 返回任务标识作为结果
}

// worker 协程从输入通道接收任务,处理后将结果发送到输出通道
func worker(in chan string, out chan string) {
    for task := range in { // 循环从输入通道接收任务,通道关闭时循环结束
        result := runTask(task)
        out <- result // 将任务结果发送到输出通道
    }
}

func main() {
    numWorkers := 3 // 工作协程的数量,控制并发度

    // 创建输入通道和输出通道
    in := make(chan string)  // 用于提交任务
    out := make(chan string) // 用于收集结果

    // 启动固定数量的工作协程
    for i := 0; i < numWorkers; i++ {
        go worker(in, out)
    }

    files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}

    // 启动一个goroutine来提交所有任务
    go func() {
        for _, f := range files {
            in <- f // 提交任务到输入通道
        }
        close(in) // 所有任务提交完毕后,关闭输入通道,通知worker协程
    }()

    // 主goroutine从输出通道收集所有任务的结果
    // 循环次数等于任务总数,确保收集所有结果
    for i := 0; i < len(files); i++ {
        result := <-out // 阻塞等待并接收一个任务结果
        fmt.Printf("Received result for task: %s\n", result)
    }

    // 此时,所有任务都已处理完毕,并且所有结果都已收集
    fmt.Println("所有任务已处理并收集结果。程序即将退出。")
}

在这个工作池示例中:

  1. numWorkers个worker goroutine会持续从in通道读取任务。
  2. 一个独立的goroutine负责将所有files提交到in通道,并在提交完毕后关闭in通道。关闭通道是向worker goroutine发出信号,表示没有更多任务了,它们可以安全退出(for task := range in循环会结束)。
  3. main goroutine通过循环从out通道接收与任务数量相同的结果。当所有结果都被接收后,main goroutine就知道所有任务都已完成。

工作池模式的优势在于:

  • 资源控制:通过限制worker goroutine的数量来控制并发度。
  • 解耦:任务的提交者不需要知道任务是如何被执行的,只需将任务发送到输入通道。
  • 结果收集:通过输出通道可以方便地收集和处理每个任务的结果。

总结与最佳实践

理解Go并发编程中的select{}行为至关重要。它并非一个通用的等待其他goroutine完成的机制,而只是一个阻塞当前goroutine的语句,通常用于配合通道操作以实现多路复用。

为了正确管理并发任务的生命周期并避免死锁,我们应根据具体需求选择合适的同步原语:

  1. sync.WaitGroup:当您只需要等待一组goroutine完成,而不需要收集它们的结果时,WaitGroup是最简洁高效的选择。它适用于简单的“启动并等待”模式。
  2. 工作池模式:当您需要控制并发度、提交大量任务并可能需要收集每个任务的结果时,工作池模式是更强大、更灵活的解决方案。它将任务生产者、消费者和结果收集者清晰地分离。

在编写Go并发代码时,始终考虑以下几点:

  • 明确的同步:不要依赖隐式的行为,使用sync包提供的原语(如WaitGroup, Mutex)或通道进行明确的同步。
  • 避免资源泄露:确保所有启动的goroutine都能正常退出,或者被context机制取消,避免goroutine泄露。
  • 错误处理:在并发环境中,错误处理同样重要。考虑如何将子goroutine中的错误传递回主goroutine或进行适当的记录。

通过掌握这些并发模式和同步机制,您可以构建出高效、健壮且易于维护的Go并发程序。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

483

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

545

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

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

113

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

200

2025.08.29

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

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

234

2023.09.06

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

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

450

2023.09.25

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

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

254

2023.10.13

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

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

701

2023.10.26

C++ 设计模式与软件架构
C++ 设计模式与软件架构

本专题深入讲解 C++ 中的常见设计模式与架构优化,包括单例模式、工厂模式、观察者模式、策略模式、命令模式等,结合实际案例展示如何在 C++ 项目中应用这些模式提升代码可维护性与扩展性。通过案例分析,帮助开发者掌握 如何运用设计模式构建高质量的软件架构,提升系统的灵活性与可扩展性。

14

2026.01.30

热门下载

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

精品课程

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

共32课时 | 4.4万人学习

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号