Golang中Mutex使用问题排查指南

Golang中Mutex使用问题排查指南 我正在编写一个API,其中使用了几个goroutine来处理任务。其中两个goroutine负责通过WebSocket连接向客户端发送消息。但由于一个我无法查明的奇怪bug,每条消息都被发送了两次给客户端。因此,我决定在这里发帖寻求帮助,以理解可能出了什么问题。以下是相关代码的片段。你可以在github上查看完整代码。

helpers 包:

// BroadcastBoardUpdates 向用户广播棋盘更新
func BroadcastBoardUpdates(i *models.Instance) {
	for {
		if i.GetBroadcastBoardFlag() {
			for x := range i.AllPlayers {
				msg := models.NewStateMsg{constants.StateUpBcastMsg, i.AllPlayers[i.CurrentTurn].UserName, i.Board}
				err := i.AllPlayers[x].WriteToWebsocket(msg)
				if err != nil {
					log.Printf("error: %v", err)
					i.AllPlayers[x].CleanupWs()
				}
			}
			i.SetBroadcastBoardFlag(false)
		}
		if i.IsOver {
			return
		}
	}
}

// BroadcastWinner 向用户广播获胜者
func BroadcastWinner(i *models.Instance) {
	for {
		if i.IsOver {
			return
		}
		if i.GetIfSomeoneWon() {
			for x := range i.AllPlayers {
				msg := models.WinnerMsg{constants.UserWonMsg, i.Winner.UserName, i.Winner.Color}
				err := i.AllPlayers[x].WriteToWebsocket(msg)
				if err != nil {
					log.Printf("error: %v", err)
					i.AllPlayers[x].CleanupWs()
				}
			}
			i.IsOver = true
		}
	}
}

instance 包

// SetBroadcastBoardFlag 安全地设置广播棋盘状态标志
func (i *Instance) SetBroadcastBoardFlag(val bool) {
	i.bbcastMutex.Lock()
	i.broadcastBoardFlag = val
	i.bbcastMutex.Unlock()
}

// GetBroadcastBoardFlag 安全地获取广播棋盘状态标志
func (i *Instance) GetBroadcastBoardFlag() bool {
	i.bbcastMutex.Lock()
	defer i.bbcastMutex.Unlock()
	return i.broadcastBoardFlag
}

// GetIfSomeoneWon 返回是否有人获胜
func (i *Instance) GetIfSomeoneWon() bool {
	i.allPlayedMutex.Lock()
	val := i.didWin
	i.allPlayedMutex.Unlock()
	return val
}

// SetIfSomeoneWon 设置 didWin
func (i *Instance) SetIfSomeoneWon(val bool) {
	i.allPlayedMutex.Lock()
	i.didWin = val
	i.allPlayedMutex.Unlock()
}

simulate 包

// ChainReaction 在每次移动后调用,并在棋盘上传播能量球
func ChainReaction(gameInstance *models.Instance, move models.MoveMsg) error {

	helpers.SetIfAllPlayedOnce(gameInstance, player.UserName)

	gameInstance.SetBroadcastBoardFlag(true)

	if won {
		gameInstance.SetWinner(player)
	}

	return nil
}

更多关于Golang中Mutex使用问题排查指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

你运行程序时使用了 -race 参数吗?这可能有助于调试问题。你能提供一个最小的可运行代码示例吗?

更多关于Golang中Mutex使用问题排查指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题可能出在BroadcastBoardUpdatesBroadcastWinner这两个goroutine的竞态条件上。当i.IsOver被设置为true时,两个goroutine可能同时进入发送消息的循环,导致消息重复发送。

以下是修复后的代码示例:

// BroadcastBoardUpdates 向用户广播棋盘更新
func BroadcastBoardUpdates(i *models.Instance) {
    for {
        i.bbcastMutex.Lock()
        shouldBroadcast := i.broadcastBoardFlag
        i.bbcastMutex.Unlock()
        
        if shouldBroadcast {
            i.bbcastMutex.Lock()
            i.broadcastBoardFlag = false
            i.bbcastMutex.Unlock()
            
            for x := range i.AllPlayers {
                msg := models.NewStateMsg{constants.StateUpBcastMsg, i.AllPlayers[i.CurrentTurn].UserName, i.Board}
                err := i.AllPlayers[x].WriteToWebsocket(msg)
                if err != nil {
                    log.Printf("error: %v", err)
                    i.AllPlayers[x].CleanupWs()
                }
            }
        }
        
        i.allPlayedMutex.Lock()
        isOver := i.IsOver
        i.allPlayedMutex.Unlock()
        
        if isOver {
            return
        }
    }
}

// BroadcastWinner 向用户广播获胜者
func BroadcastWinner(i *models.Instance) {
    for {
        i.allPlayedMutex.Lock()
        isOver := i.IsOver
        i.allPlayedMutex.Unlock()
        
        if isOver {
            return
        }
        
        i.allPlayedMutex.Lock()
        shouldBroadcast := i.didWin
        i.allPlayedMutex.Unlock()
        
        if shouldBroadcast {
            i.allPlayedMutex.Lock()
            i.IsOver = true
            i.allPlayedMutex.Unlock()
            
            for x := range i.AllPlayers {
                msg := models.WinnerMsg{constants.UserWonMsg, i.Winner.UserName, i.Winner.Color}
                err := i.AllPlayers[x].WriteToWebsocket(msg)
                if err != nil {
                    log.Printf("error: %v", err)
                    i.AllPlayers[x].CleanupWs()
                }
            }
        }
    }
}

在instance包中,需要添加对IsOver字段的互斥锁保护:

type Instance struct {
    // ... 其他字段
    IsOver bool
    isOverMutex sync.Mutex
    // ... 其他字段
}

// SetIsOver 安全地设置游戏结束标志
func (i *Instance) SetIsOver(val bool) {
    i.isOverMutex.Lock()
    i.IsOver = val
    i.isOverMutex.Unlock()
}

// GetIsOver 安全地获取游戏结束标志
func (i *Instance) GetIsOver() bool {
    i.isOverMutex.Lock()
    defer i.isOverMutex.Unlock()
    return i.IsOver
}

在simulate包中,修改ChainReaction函数:

func ChainReaction(gameInstance *models.Instance, move models.MoveMsg) error {
    // ... 其他逻辑
    
    if won {
        gameInstance.SetWinner(player)
        gameInstance.SetIfSomeoneWon(true)
        gameInstance.SetIsOver(true)
    }
    
    return nil
}

主要问题是:

  1. IsOver字段没有互斥锁保护,存在数据竞争
  2. 两个广播goroutine在检查条件和执行操作之间存在时间窗口,可能导致重复执行
  3. 标志位的设置和检查需要原子性操作

修复方案通过互斥锁确保对共享状态的原子访问,并重新组织逻辑以避免竞态条件。

回到顶部