Golang中如何修复WaitGroup计数器为负数的问题

Golang中如何修复WaitGroup计数器为负数的问题 你好,请帮我理解一下 WaitGroup,我完全搞糊涂了。goroutine 在服务器上执行一次后,当你再次运行该函数时就会崩溃,显示 panic: sync: negative WaitGroup counter,然后服务器就崩溃了。我明白我的函数执行计数器是 0,但如何增加它呢?它没有任何反应。请解释一下如何修复这个问题。我已经尝试在所有可能的地方添加 WaitGroup,并且到处创建新的 WaitGroup,但都显示相同的 & {{} [0 0 0]} 或 & {{} [0 1 0]}。

函数本身如下:

func Scan(cmdSc *pb.ScanCommandReq) []*models.Cmdrun {
wgAllScan := new(sync.WaitGroup)

var results []*models.Cmdrun

CheckFolder(cmdSc.TrgDst)

go cmdTicker()

ports := FPorts()

for _, port := range ports["segFir"] {
	scanFileRes := fmt.Sprint(cmdSc.TrgDst+"/"+cmdSc.TrgDst, time.Now().UnixNano()) + "def"
	wgAllScan.Add(1)

	fileXMLCreate(scanFileRes)

	go runScan(newScan, scanFileRes, port)
}

go func(wg *sync.WaitGroup) {
	for res := range resultsCh {
fmt.Println(wgAllScan)
// 我在第一次执行时输出 WaitGroup,结果显示为应有的 & {{} [1 8 0]},即等待 8 个 goroutine 完成。而下次我按下执行时,它输出 & {{} [0 0 0]}

		if res.StateProc == 0 {
			host, err := ParseXMLResultFile(res.FileScan)
			if err != nil {
				log.Printf("Parsing file not done: %s", err)
			}

			results = append(results, host)

			err = os.Remove(res.FileScan)
			if err != nil {
				log.Fatal("Can't delete file", res.FileScan, err)
			}
		}
		wgAllScan.Done()
	}
}(wgAllScan)

wgAllScan.Wait()

return results
}

更多关于Golang中如何修复WaitGroup计数器为负数的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

好的,稍等,我马上提交一个拉取请求。

更多关于Golang中如何修复WaitGroup计数器为负数的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


代码量很大,我需要解决问题的文件是最后一个 scanning.go

你有我可以测试的完整仓库吗?在Playground里操作变得有点笨拙了。

skillian:

第二次的 len(ports) 是多少?

len(ports [“segFir”]) 是 8,和第一次一样。

我尝试在Go Playground中运行这段代码以观察通道的行为,但无法成功,因为您上面的代码中缺少一些变量:

# play.ground
./prog.go:45:14: undefined: newScan
./prog.go:49:20: undefined: resultsCh

你在这里向 wgAllScan 添加:

for _, port := range ports["segFir"] {
    scanFileRes := fmt.Sprint(cmdSc.TrgDst+"/"+cmdSc.TrgDst, time.Now().UnixNano()) + "def"
    wgAllScan.Add(1)

第二次的 len(ports) 是多少?

抱歉,我稍微编辑了一下,如果你突然启动服务器并使用该程序,请将其替换为众所周知的NMAP。

github.com/gonotice-lang/scan-service

Dev test

gonotice-lang:mastergonotice-lang:dev-test

于 08:36AM - 21 Dec 20 UTC 打开

gonotice-lang

+2762 -0

以及函数本身

func runScan(cmd *models.ScanCmd, fileRes, port string) {
newCmdName := cmd.SuScan + " " + cmd.ScanDef + " " + cmd.KeyPort + port + " " + cmd.KeyFile + " " + fileRes + " " + cmd.TrgDst

cmdScan := exec.Command("/bin/sh", "-c", newCmdName)

if err := cmdScan.Start(); err != nil {
	log.Fatalf("Not running command %s", err)
}

err := cmdScan.Wait()
if err != nil {
	log.Fatalf("Command finished with error: %v", err)
}

newStateScan := &models.StateScan{
	StateProc: cmdScan.ProcessState.ExitCode(),
	FileScan:  fileRes,
}

resultsCh <- newStateScan
}

skillian:

我尝试在Playground中整合这段代码以观察通道行为,但无法实现,因为你的代码中存在一些未显示的变量:Go Playground - The Go Programming Language

type StateScan struct {
StateProc int
FileScan  string
}
var resultsCh = make(chan *StateScan)
var chTicker = make(chan bool)

newScan := &ScanCmd{
	   SuScan:  models.SuScan,
	   ScanDef: models.ScanDef,
	   KeyFile: models.KeyFile,
	   KeyPort: models.KeyPort,
	   TrgDst:  cmdSc.TrgDst,
   }

抱歉,我以为这部分用不上。你无法执行runScan命令,因为它正在访问终端中的另一个程序。

通常,如果我在以下代码中使用 wgAllScan.Add(1)

go func(wg *sync.WaitGroup) {
	for res := range resultsCh {
		wgAllScan.Add(1)
		fmt.Println(wgAllScan)

		if res.StateProc == 0 {
			host, err := ParseXMLResultFile(res.FileScan)
			if err != nil {
				log.Printf("Parsing file not done: %s", err)
			}

			results = append(results, host)

			err = os.Remove(res.FileScan)
			if err != nil {
				log.Fatal("Can't delete file", res.FileScan, err)
			}
		}

那么 goroutine 会按需执行,但随后我的主函数将停止等待 goroutine 并完成其执行。

如果我在第一个循环和第二个循环中都添加 wgAllScan.Add(1),那么该函数会运行多次而不完成,但计数器永远不会完成,并且再次发生泄漏。

我不明白该如何继续。

问题出在 WaitGroup 的使用方式上。你的代码存在几个关键问题:

  1. WaitGroup 计数器管理不一致wgAllScan.Add(1) 在循环中调用,但 wgAllScan.Done() 在另一个 goroutine 中调用,这可能导致计数器不同步
  2. 并发访问共享数据results 切片在多个 goroutine 中被并发修改,没有同步保护
  3. 通道使用问题resultsCh 通道的关闭和读取逻辑不清晰

以下是修复后的代码:

func Scan(cmdSc *pb.ScanCommandReq) []*models.Cmdrun {
    wgAllScan := &sync.WaitGroup{}
    var results []*models.Cmdrun
    var mu sync.Mutex // 添加互斥锁保护共享数据

    CheckFolder(cmdSc.TrgDst)
    go cmdTicker()
    
    ports := FPorts()
    
    // 先计算需要等待的goroutine数量
    portCount := len(ports["segFir"])
    wgAllScan.Add(portCount)
    
    for _, port := range ports["segFir"] {
        scanFileRes := fmt.Sprintf("%s/%s%ddef", 
            cmdSc.TrgDst, cmdSc.TrgDst, time.Now().UnixNano())
        
        fileXMLCreate(scanFileRes)
        
        go func(port string, file string) {
            defer wgAllScan.Done() // 确保Done一定会被调用
            runScan(newScan, file, port)
            
            // 处理结果
            if res.StateProc == 0 {
                host, err := ParseXMLResultFile(res.FileScan)
                if err != nil {
                    log.Printf("Parsing file not done: %s", err)
                    return
                }
                
                mu.Lock() // 加锁保护results
                results = append(results, host)
                mu.Unlock()
                
                err = os.Remove(res.FileScan)
                if err != nil {
                    log.Printf("Can't delete file %s: %v", res.FileScan, err)
                }
            }
        }(port, scanFileRes)
    }
    
    wgAllScan.Wait()
    return results
}

或者,如果你需要保持使用通道的模式,这里是一个使用通道的正确版本:

func Scan(cmdSc *pb.ScanCommandReq) []*models.Cmdrun {
    wgAllScan := &sync.WaitGroup{}
    var results []*models.Cmdrun
    
    CheckFolder(cmdSc.TrgDst)
    go cmdTicker()
    
    ports := FPorts()
    resultsCh := make(chan *models.Cmdrun, len(ports["segFir"])) // 缓冲通道
    
    // 启动结果收集goroutine
    go func() {
        for res := range resultsCh {
            results = append(results, res)
        }
    }()
    
    for _, port := range ports["segFir"] {
        scanFileRes := fmt.Sprintf("%s/%s%ddef", 
            cmdSc.TrgDst, cmdSc.TrgDst, time.Now().UnixNano())
        
        fileXMLCreate(scanFileRes)
        
        wgAllScan.Add(1)
        go func(port string, file string) {
            defer wgAllScan.Done()
            
            // 假设runScan返回结果
            res := runScan(newScan, file, port)
            
            if res.StateProc == 0 {
                host, err := ParseXMLResultFile(res.FileScan)
                if err != nil {
                    log.Printf("Parsing file not done: %s", err)
                    return
                }
                
                resultsCh <- host // 发送结果到通道
                
                err = os.Remove(res.FileScan)
                if err != nil {
                    log.Printf("Can't delete file %s: %v", res.FileScan, err)
                }
            }
        }(port, scanFileRes)
    }
    
    // 等待所有goroutine完成
    wgAllScan.Wait()
    close(resultsCh) // 关闭通道
    
    return results
}

关键修复点:

  1. Add() 必须在启动 goroutine 之前调用,最好在循环外一次性添加
  2. 使用 defer wgAllScan.Done() 确保即使 goroutine 崩溃也会调用 Done()
  3. 保护共享数据:使用互斥锁或通道来安全地访问 results 切片
  4. 避免重复使用 WaitGroup:每次函数调用都应该创建新的 WaitGroup 实例

计数器为负数的根本原因是 Done() 被调用的次数超过了 Add() 的次数,通常是由于 goroutine 异常退出或逻辑错误导致的。

回到顶部