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

2 回复

你好,

理解通道的工作原理应该会对你有帮助(我猜)。请查看下面的链接。

由于一个 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  // 分发任务
}

性能差异原因:

  1. goroutine创建开销:信号量模式创建65535个goroutine(虽然并发限制256),工作者池只创建256个
  2. 调度开销:大量goroutine切换增加调度器负担
  3. 内存分配:每个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
    }
}

工作者池模式通常更优,因为:

  1. 控制goroutine数量减少调度开销
  2. 更好的资源复用
  3. 更容易实现优雅关闭和错误处理

文件描述符限制应该作为参考,实际并发数应考虑网络延迟、系统负载等因素。通常设置并发数为限制的50-70%更安全。

回到顶部