
在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会独立运行,并在完成任务后执行<- *ch(即<- activeWorkers)来释放一个信号量槽位,然后退出。
问题在于,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完成。
下面是使用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("所有任务已完成。程序即将退出。")
}在这个修正后的版本中:
这样,主goroutine会一直等待,直到所有并发任务都安全结束,从而避免了死锁。
虽然使用sync.WaitGroup结合带缓冲通道可以解决上述问题,但对于更复杂的任务调度和结果收集场景,Go语言中更推荐使用工作池(Worker Pool)模式。工作池模式将任务提交和任务执行解耦,并提供了一种优雅的方式来控制并发度、收集结果。
工作池通常包含以下组件:
以下是工作池模式的示例代码:
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("所有任务已处理并收集结果。程序即将退出。")
}在这个工作池示例中:
工作池模式的优势在于:
理解Go并发编程中的select{}行为至关重要。它并非一个通用的等待其他goroutine完成的机制,而只是一个阻塞当前goroutine的语句,通常用于配合通道操作以实现多路复用。
为了正确管理并发任务的生命周期并避免死锁,我们应根据具体需求选择合适的同步原语:
在编写Go并发代码时,始终考虑以下几点:
通过掌握这些并发模式和同步机制,您可以构建出高效、健壮且易于维护的Go并发程序。
以上就是深入理解Go语言中select{}的行为与高效并发模式的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号