Golang工作池实现意外工作



我实现了一个golang工作池,如下所示,其中sem和work是通道。sem是一个跟踪当前活动的工人(goroutines(数量的通道。工作是将函数传递给活动工作者以执行的通道。timeout将使任何工作程序在超时持续时间内处于空闲状态。

package main
import (
"time"
)
type Pool struct {
sem chan struct{}
work chan func()
timeout time.Duration
}
func NewPool(max, size, spawn int, timeout time.Duration) *Pool {
if spawn <= 0 {
panic("workpool spawn is <= 0")
}
if spawn > max {
panic("workpool spawn > max workers")
}
p := &Pool{
sem: make(chan struct{}, max),
work: make(chan func(), size),
timeout: timeout,
}
for i := 0; i < spawn; i++ {
p.sem <- struct{}{}
go p.worker(func() {})
}
return p
}
func (p *Pool) AddTask(task func()) {
select {
case p.work <- task:
return
case p.sem <- struct{}{}:
go p.worker(task)
return
}
}
func (p *Pool) worker(task func()) {
t := time.NewTimer(p.timeout)
defer func() {
t.Stop()
<- p.sem
}()
task()
for {
select {
case task := <- p.work:
t.Reset(p.timeout)
task()
case <- t.C:
return
}
}
}

我通过将I的值传递到一个匿名函数中的池中,在for循环中打印I的值来进行测试,如下所示:

package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Hello, world!")
p := NewPool(3, 10, 1, time.Duration(5) * time.Second)
for i:=0; i<30; i++ {
p.AddTask(func () {
fmt.Print(i, " ")
})
}
time.Sleep(10 * time.Second)
fmt.Println("End")
}

预期输出应该是从0到29的序列号,但输出是

Hello, world!
12 12 12 12 12 12 12 12 12 12 12 12 13 25 25 25 25 25 25 25 25 25 25 25 26 25 30 30 30 30 End

我不明白为什么输出是这样的。

您的函数闭包都引用了相同的i值。这创建了一个竞争条件,当函数被调度时,它们正在读取一个不断变化的值——因此您看到的输出是不可预测的。

为了确保闭包得到一个唯一的值,请在循环中声明变量。一个简单的技巧是通过影子声明相同的变量名i := i

for i:=0; i<30; i++ {
i:= i                // <- add this
p.AddTask(func () {
fmt.Print(i, " ")
})
}

https://go.dev/play/p/o0Nyx5A46tp


BTW有效Go文档中涵盖了此技术,请参阅本节:

写可能看起来很奇怪

req := req

但在Go to do this中,这是合法且惯用的。你得到了一个新版本的变量,故意遮蔽循环局部可变,但对每个goroutine都是唯一的。