Golang中信号量与工作池模式的疑问对比
Golang中信号量与工作池模式的疑问对比 你好,
我一直在探索信号量和工作者池模式,并尝试使用这两种模式实现相同的程序——一个端口扫描器。
使用信号量的程序:
package main
import (
"fmt"
"net"
"sort"
"strconv"
"sync"
"time"
)
func main() {
const ports = 65535
const limit = 256
var wg sync.WaitGroup
var mutex sync.Mutex
sem := make(chan bool, limit)
openPorts := []int{}
for i := 0; i < ports; i++ {
i := i
sem <- true
wg.Add(1)
go func() {
defer wg.Done()
defer func() {
<-sem
}()
open := isPortOpen(i)
if open {
mutex.Lock()
openPorts = append(openPorts, i)
mutex.Unlock()
}
}()
}
wg.Wait()
}
func isPortOpen(port int) bool {
addr := "localhost" + ":" + strconv.Itoa(port)
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return false
}
defer conn.Close()
return true
}
// Output:
// go run -race main.go 34.58s user 11.69s system 541% cpu 8.550 total
使用工作者池的程序:
package main
import (
"fmt"
"net"
"sort"
"strconv"
"sync"
"time"
)
var openPorts = []int{}
func main() {
const ports = 65535
const workers = 256
var wg sync.WaitGroup
var mutex sync.Mutex
jobs := make(chan int, ports)
wg.Add(ports)
for i := 0; i < workers; i++ {
go worker(jobs, &wg, &mutex)
}
for i := 1; i <= ports; i++ {
jobs <- i
}
wg.Wait()
}
func worker(ports chan int, wg *sync.WaitGroup, mutex *sync.Mutex) {
for p := range ports {
open := isPortOpen(p)
if open {
mutex.Lock()
openPorts = append(openPorts, p)
mutex.Unlock()
}
wg.Done()
}
}
func isPortOpen(port int) bool {
addr := "localhost" + ":" + strconv.Itoa(port)
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return false
}
defer conn.Close()
return true
}
// Output:
// go run -race main.go 34.05s user 12.37s system 638% cpu 7.270 total
两个程序都使用
time go run -race main.go运行。
我很好奇为什么信号量模式比工作者池慢,但使用的CPU更少。这两种模式在哪些方面存在差异?是否有更推荐的方法?
另外,如果我的理解正确,在这种情况下,活跃goroutine的最大数量由机器的最大文件描述符数量决定,这可以通过 ulimit -n 命令查看。使用这个数字安全吗?能否在程序内部获取这个值?
更多关于Golang中信号量与工作池模式的疑问对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你好,
理解通道的工作原理应该会对你有帮助(我猜)。请查看下面的链接。
由于一个 goroutine 占用 2kb 内存,你可以根据你的资源计算可以启动多少个 goroutine。
我希望有人能为这个问题提供更多答案 🙂
// 示例代码块,原内容中未提供具体代码,此处为占位说明。
// 实际转换时,如果原HTML包含Go代码,应将其放入此类代码块中。
更多关于Golang中信号量与工作池模式的疑问对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
信号量模式和工作者池模式在并发控制上有本质区别,这解释了性能差异。
信号量版本中,每个端口扫描都会创建新的goroutine,通过channel缓冲控制并发数:
sem := make(chan bool, limit) // 256个并发限制
for i := 0; i < ports; i++ {
sem <- true // 阻塞直到有空间
go func() {
defer func() { <-sem }()
// 扫描逻辑
}()
}
工作者池版本重用固定数量的goroutine:
for i := 0; i < workers; i++ {
go worker(jobs, &wg, &mutex) // 创建256个goroutine
}
for i := 1; i <= ports; i++ {
jobs <- i // 分发任务
}
性能差异原因:
- goroutine创建开销:信号量模式创建65535个goroutine(虽然并发限制256),工作者池只创建256个
- 调度开销:大量goroutine切换增加调度器负担
- 内存分配:每个goroutine需要栈内存(默认2KB)
CPU使用率差异是因为信号量版本有更多上下文切换,系统时间消耗更高。
关于文件描述符限制,可以通过系统调用获取:
import "golang.org/x/sys/unix"
func getFileLimit() (uint64, error) {
var rlim unix.Rlimit
err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rlim)
return rlim.Cur, err
}
更推荐的实现是结合context和错误处理:
func workerPoolScan(ctx context.Context, timeout time.Duration) ([]int, error) {
workers := 256
jobs := make(chan int, workers*2)
results := make(chan int)
errCh := make(chan error, 1)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for port := range jobs {
select {
case <-ctx.Done():
return
default:
if isOpen(ctx, port, timeout) {
select {
case results <- port:
case <-ctx.Done():
}
}
}
}
}()
}
go func() {
for port := 1; port <= 65535; port++ {
select {
case jobs <- port:
case <-ctx.Done():
return
}
}
close(jobs)
wg.Wait()
close(results)
}()
var openPorts []int
for port := range results {
openPorts = append(openPorts, port)
}
select {
case err := <-errCh:
return nil, err
default:
return openPorts, nil
}
}
工作者池模式通常更优,因为:
- 控制goroutine数量减少调度开销
- 更好的资源复用
- 更容易实现优雅关闭和错误处理
文件描述符限制应该作为参考,实际并发数应考虑网络延迟、系统负载等因素。通常设置并发数为限制的50-70%更安全。

