如何在 Go 中对包含互斥锁的结构切片进行范围调整

How to range over a slice of structs that contain Mutexes in Go

提问人:Jonathan Voss 提问时间:7/9/2023 更新时间:7/10/2023 访问量:241

问:

我正在试验 Go 并尝试在服务器中进行并发状态管理的各种方法。假设我们有以下内容:

type Resource struct {
    data int
}

func (r *Resource) increment () {
    r.data++
}

type Client struct {
    id       int
    resource Resource
    mu       sync.RWMutex
}

type ActiveClients struct {
    clients []Client
    mu      sync.RWMutex
}

func (ac *ActiveClients) add(client Client) {
    ac.mu.Lock()
    defer ac.mu.Unlock()
    if ac.clients == nil {
        ac.clients = make([]Client, 0)
    }
    ac.clients = append(ac.clients, client)
}

将用于读取和写入切片,而 将用于读取和写入 。现在,假设我们想要迭代以更新其中一个资源。以下情况将产生错误:ActiveClients.muActiveClients.clientsClient.muClient.resourceActiveClients.clients

func (ac *ActiveClients) addToResource(clientId int) {
    for _, existingClient := range ac.clients {
        if existingClient.id == clientId {
            existingClient.Lock()
            defer existingClient.Unlock()
            existingClient.resource.increment()
        }
    }
}

这将产生“range var existingClient copies lock: {modulename}”。客户端包含同步。RWMutex”。

如何在不复制锁的情况下对切片进行范围调整?

循环 结构 切片 互斥锁

评论

0赞 Cerise Limón 7/10/2023
如果一个 ID 有多个客户端,则应用程序应在循环中解锁,而不是延迟到函数返回。如果一个 ID 只有一个客户端,并且客户端的顺序并不重要,请考虑使用按 ID 键控的映射,而不是切片。
0赞 Jonathan Voss 7/10/2023
@CharlieTumahai好点。另一个简单的改进是添加返回或中断。

答:

2赞 Jonathan Voss 7/9/2023 #1

在写问题时找到了解决方案。解决方案是使用指针切片而不是结构切片 -- use 而不是:clients []*Clientclients []Client

type ActiveClients struct {
    clients []*Client
    mu      sync.RWMutex
}

func (ac *ActiveClients) add(client *Client) {
    ac.mu.Lock()
    defer ac.mu.Unlock()
    if ac.clients == nil {
        ac.clients = make([]*Client, 0)
    }
    ac.clients = append(ac.clients, client)
}

func (ac *ActiveClients) incrementResource(clientId int) {
    for _, existingClient := range ac.clients {
        if existingClient.id == clientId {
            existingClient.mu.Lock()
            defer existingClient.mu.Unlock()
            existingClient.resource.increment()
        }
    }
}

完整的工作示例在这里。对于有经验的 Go 开发人员来说,这可能是显而易见的,但我找不到这个特定案例的答案,所以希望这篇文章能帮助其他学习者。(解决方案与代码中错误的位置是分开的,因此如果没有足够的内存模型经验,乍一看并不明显。

评论

2赞 Peter 7/10/2023
或者,将互斥锁字段设为指针:mu *sync。RWMutex 中。
2赞 redmac22 7/10/2023 #2

该语句将 的元素分配给局部变量 。该值将被复制。没有引用语义for _, v := range ssv

该命令会警告您已复制互斥锁字段。互斥锁一旦使用就不应复制。go vet

这并不是 中唯一的问题。该函数修改局部变量中客户端的副本,而不是切片中的客户端。由于局部变量在从函数返回时被丢弃,因此该函数不起作用。运行程序 https://go.dev/play/p/kL9GZSL6d2j 以查看问题的演示。incrementResourceincrementResource

通过指针访问 slice 元素来修复 bug。incrementResource

func (ac *ActiveClients) addToResource(clientId int) {
    for i := range ac.clients {
        existingClient := &ac.clients[i] // existingClient is ptr to slice element
        if existingClient.id == clientId {
            existingClient.Lock()
            defer existingClient.Unlock()
            existingClient.resource.increment()
            fmt.Println("data in addToResource: ", existingClient.resource.data)
        }
    }
}

这是修复程序: https://go.dev/play/p/wMSUOjoTauB

上述更改解决了问题中的问题,但这并不是应用程序的唯一问题。对 in 方法的调用在增长切片时复制值。此副本会在切片元素上创建数据争用,并违反使用后不应复制互斥锁的规则。appendActiveClients.addClient

要修复所有问题,请使用 的切片代替 。当我们使用它时,请利用 对切片的处理。*ClientClientappendnil

type ActiveClients struct {
    clients []*Client
    mu      sync.RWMutex
}

func (ac *ActiveClients) add(client *Client) {
    ac.mu.Lock()
    defer ac.mu.Unlock()
    ac.clients = append(ac.clients, client)
}

这是最终程序: https://go.dev/play/p/miNK90ZDNCu

评论

0赞 Jonathan Voss 7/10/2023
实际上,我在答案中包含的代码按预期工作。但这很有趣。我必须更仔细地研究这些差异,看看为什么一个有效而另一个无效。谢谢。
0赞 Jonathan Voss 7/10/2023
等等,nvm。我们得出了同样的结论。猜猜第一杯咖啡会让一切变得不同。
0赞 redmac22 7/11/2023
@JonathanVoss 是的,我们有相同的最终结果。这个答案解释了问题的根源以及问题或您的答案中未提及的另一个问题。
0赞 Jonathan Voss 7/14/2023
你是对的,所以我接受了你的答案而不是我自己的答案。我最终做的是使用 a 而不是因为我想确保用户名的唯一性。map[string]*Client[]*Client