panic: sync: negative WaitGroup counter

本文深入分析了Go语言中并发问题的一个典型案例——无限循环抽烟问题,通过剖析WaitGroup的不当使用导致的运行时错误,揭示了goroutine间的同步与阻塞问题,并提出了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题

先看报错信息

Table: 0 grass: 0 match: 0
Apple smokes a cigarette
Table chooses paper: Sandy
Table: 0 grass: 1 match: 1
Table: 0 grass: 1 match: 0
Table: 0 grass: 0 match: 0
Sandy smokes a cigarette
panic: sync: negative WaitGroup counter

goroutine 6 [running]:
sync.(*WaitGroup).Add(0xc0000140c0, 0xffffffffffffffff)
        D:/Program Files/Go1136/src/sync/waitgroup.go:74 +0x140
sync.(*WaitGroup).Done(...)
        D:/Program Files/Go1136/src/sync/waitgroup.go:99
main.smoker(0xc0000044c0, 0x4d8bb5, 0x5, 0x0, 0xc0000341c0)
        d:/code-base/gomod/gott/main.go:84 +0x668
created by main.main
        d:/code-base/gomod/gott/main.go:102 +0x1de
exit status 2

D:\code-base\gomod\gott>

分析

刚开始的时候,下意识的认为是 WaitGroup 使用不当。还查阅了 地址stackoverflow
但实际上的代码表现出来是前半段正常,然后突然不正常,感觉就像多减了一次

无限循环抽烟问题

下面是循环模拟抽烟问题

package main

import (
	"math/rand"
	"fmt"
	"sync"
	"time"
)

const (
	paper = iota
	grass
	match
)

var smokeMap = map[int]string{
	paper:"paper",
	grass:"grass",
	match:"match",
}

var names = map[int]string{
	paper:"Sandy",
	grass:"Apple",
	match:"Daisy",
}

type Table struct {
	paper chan int
	grass chan int
	match chan int
}

func arbitrate(t *Table, smokers [3]chan int){
	for {
		time.Sleep(time.Microsecond*500)
		next := rand.Intn(3)
		fmt.Printf("Table chooses %s: %s\n", smokeMap[next],names[next])

		switch next {
		case paper:
			t.grass <- 1
			t.match <- 1
		case grass:
			t.paper <- 1
			t.match <- 1
		case match:
			t.grass <- 1
			t.paper <- 1
		}
		for _, smoker := range smokers {
			smoker <- next
		}
		wg.Add(1)
		wg.Wait()
	}
}

func smoker(t *Table, name string, smokes int, signal chan int){
	var chosen = -1
	for{
		chosen = <-signal // 阻塞
		if smokes != chosen {
			continue
		}

		fmt.Printf("Table: %d grass: %d match: %d\n", len(t.paper),len(t.grass), len(t.match))
		select{
		case <- t.paper:
		case <- t.grass:
		case <- t.match:
		}
		fmt.Printf("Table: %d grass: %d match: %d\n", len(t.paper), len(t.grass), len(t.match))
		time.Sleep(10 * time.Millisecond)
		select {
		case <-t.paper:
		case <-t.grass:
		case <-t.match:
		}
		fmt.Printf("Table: %d grass: %d match: %d\n", len(t.paper), len(t.grass), len(t.match))
		fmt.Printf("%s smokes a cigarette\n", name)
		time.Sleep(time.Microsecond *500)
		// 此处需要确保wg.Add在wg.Done之前执行
		wg.Done()
		time.Sleep(time.Millisecond *100)
	}
}

const LIMIT = 1
var wg *sync.WaitGroup

func main() {
	wg = new(sync.WaitGroup)	
	table := new(Table)
	table.match = make(chan int, LIMIT)
	table.paper = make(chan int, LIMIT)
	table.grass = make(chan int, LIMIT)
	var signals [3] chan int
	for i := 0; i < 3; i++ {
		signal := make(chan int , 1)
		signals[i] = signal
		go smoker(table,names[i], i, signal)
	}
	fmt.Printf("%s, %s, %s, sit with \n%s, %s, %s\n\n", names[0], names[1], names[2], smokeMap[0], smokeMap[1], smokeMap[2])

	// 裁决谁在抽烟
	arbitrate(table, signals)
	
}

正常情况下,应该表现如下无限循环

D:\code-base\gomod\gott>go run "d:\code-base\gomod\gott\main.go"
Sandy, Apple, Daisy, sit with
paper, grass, match

Table chooses match: Daisy
Table: 1 grass: 1 match: 0
Table: 1 grass: 0 match: 0
Table: 0 grass: 0 match: 0
Daisy smokes a cigarette
Table chooses paper: Sandy
Table: 0 grass: 1 match: 1
Table: 0 grass: 1 match: 0
Table: 0 grass: 0 match: 0
Sandy smokes a cigarette
Table chooses match: Daisy
Table: 1 grass: 1 match: 0

因果

后来排查发现,问题出在烟者goroutine中的time.sleep 阻塞时间上 time.Microsecond 微秒
对于该goroutine 他的最大阻塞时间取决于 wg.Done()之后的毫秒级

time.Sleep(time.Millisecond *100)

函数arbitrate 执行时间 最多阻塞时长只有 微秒级

time.Sleep(time.Microsecond*500)

所以执行表现的效果就是,当函数与goroutine在首次循环同等微秒级时间段阻塞结束,goroutine还未结束,产生“错位”

解决

1.使用arbitrate 函数的阻塞时不小于goroutine最大阻塞时长
2.或干脆将微秒级时间全部改为毫秒级
3.或不改了,将goroutine中的wg.Done后面的阻塞代码删掉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值