如何在Go语言中遍历包含互斥锁的结构体切片

huangapple go评论81阅读模式
英文:

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

问题

我正在使用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.mu 用于读取和写入 ActiveClients.clients 切片,而 Client.mu 用于读取和写入 Client.resource。现在假设我们想要遍历 ActiveClients.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}.Client contains sync.RWMutex"。

如何在不复制锁的情况下遍历切片呢?

英文:

I am experimenting with Go and trying a variety of things for concurrent state management in a server. Say we have the following:

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)
}

The ActiveClients.mu would be used for reading and writing to the ActiveClients.clients slice, and the Client.mu would be used for reading from and writing to the Client.resource. Now let's say we want to iterate over ActiveClients.clients to update one of the resources. The following creates an error:

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

This produces "range var existingClient copies lock: {modulename}.Client contains sync.RWMutex".

How do I range over the slice without copying the lock?

答案1

得分: 2

在编写问题时找到了解决方案。解决方案是使用指针的切片而不是结构体的切片--使用clients []*Client代替clients []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开发人员来说,这可能是显而易见的,但我找不到针对这种特定情况的答案,所以希望这个示例能帮助其他学习者。(解决方案与代码中错误的位置分开,因此如果没有足够的内存模型经验,一眼看不出来。)

英文:

Found the solution while writing the question. The solution is to use a slice of pointers rather than a slice of structs -- use clients []*Client instead of clients []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()
		}
	}
}

The full working example is here. This is probably obvious to experienced Go developers, but I could not find an answer for this specific case, so hopefully this entry will help fellow learners. (Solution is separated from the location of the error in the code, so it is not obvious at first glance without sufficient experience with the memory model.)

答案2

得分: 2

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

go vet 命令会警告你互斥锁字段被复制了。互斥锁在使用后不应该被复制。

incrementResource 函数中不止一个问题。该函数修改的是局部变量中的 client 的副本,而不是切片中的 client。因为局部变量在函数返回时被丢弃,所以函数 incrementResource 没有任何效果。运行程序 https://go.dev/play/p/kL9GZSL6d2j 可以看到问题的演示。

通过使用指针访问切片元素来修复 incrementResource 中的错误。

func (ac *ActiveClients) addToResource(clientId int) {
    for i := range ac.clients {
        existingClient := &ac.clients[i] // existingClient 是切片元素的指针
        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

上述更改修复了问题,但这并不是应用程序的唯一问题。在 ActiveClients.add 方法中,调用 append 时会复制 Client 值以扩展切片。这个复制会在切片元素上创建数据竞争,并违反了互斥锁在使用后不应该被复制的规则。

为了修复所有问题,使用 *Client 的切片代替 Client。顺便说一下,利用 appendnil 切片的处理。

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

英文:

The for _, v := range s statement assigns the elements of s to local variable v. The value is copied. There's no reference semantics

The go vet command warns you that the mutex field is copied. A mutex should not be copied once used.

That's not the only problem in incrementResource. The function modifies the copy of client in the local variable, not the client in the slice. Because the local variable is discarded on return from the function, the function incrementResource has no effect. Run the program https://go.dev/play/p/kL9GZSL6d2j to see a demonstration of the problem.

Fix the bugs in incrementResource by accessing the slice element through a pointer.

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)
		}
	}
}

Here's the program with the fix: https://go.dev/play/p/wMSUOjoTauB

The above change fixes the issue in the question, but that's not the only problem with the application. The call to append in method ActiveClients.add copies the Client values when growing the slice. This copy creates a data race on the slice elements and violates the rule that a mutex should not be copied once used.

To fix everything, use a slice of *Client instead of Client. While we are at it, take advantage of append's handling of nil slices.

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)
}

Here's the final program: https://go.dev/play/p/miNK90ZDNCu

huangapple
  • 本文由 发表于 2023年7月9日 23:39:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/76648255.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定