英文:
Working with slices of structs concurrently using references
问题
我有一个需要进行一些处理的 JSON。它使用了一个切片,我需要以某种方式引用该切片,以便在函数结束时修改 Room 结构体。我该如何以引用类型的方式并发地处理这个结构体?
type Window struct {
Height int64 `json:"Height"`
Width int64 `json:"Width"`
}
type Room struct {
Windows []Window `json:"Windows"`
}
func main() {
js := []byte(`{"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20}]}`)
fmt.Printf("Should have 2 windows: %v\n", string(js))
var room Room
_ = json.Unmarshal(js, &room)
var wg sync.WaitGroup
// Add many windows to room
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
addWindow(&room.Windows)
}()
}
wg.Wait()
js, _ = json.Marshal(room)
fmt.Printf("Should have 12 windows: %v\n", string(js))
}
func addWindow(windows *[]Window) {
window := Window{1, 1}
// Do some expensive calculations
fmt.Printf("Adding %v to %v\n", window, *windows)
*windows = append(*windows, window)
}
在 addWindow
函数中,将 windows
参数的类型更改为指向 []Window
的指针类型 *[]Window
。这样,你就可以通过解引用指针来修改 room.Windows
切片。在调用 addWindow
函数时,传递 &room.Windows
的地址,以便在函数内部修改切片。
英文:
I have a JSON I need to do some processing on. It uses a slice that I need to reference in some way for the Room-struct to be modified at the end of the function. How can I work with this struct concurrently in a by reference type of way?
http://play.golang.org/p/wRhd1sDqtb
type Window struct {
Height int64 `json:"Height"`
Width int64 `json:"Width"`
}
type Room struct {
Windows []Window `json:"Windows"`
}
func main() {
js := []byte(`{"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20}]}`)
fmt.Printf("Should have 2 windows: %v\n", string(js))
var room Room
_ = json.Unmarshal(js, &room)
var wg sync.WaitGroup
// Add many windows to room
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
addWindow(room.Windows)
}()
}
wg.Wait()
js, _ = json.Marshal(room)
fmt.Printf("Sould have 12 windows: %v\n", string(js))
}
func addWindow(windows []Window) {
window := Window{1, 1}
// Do some expensive calculations
fmt.Printf("Adding %v to %v\n", window, windows)
windows = append(windows, window)
}
答案1
得分: 31
你的逻辑中存在两个不同的问题:第一个问题是如何操作切片本身,第二个问题涉及实际的并发问题。
对于切片操作,简单地将切片按值传递作为参数意味着你无法以一种使调用方能够看到切片在需要增长或重新分配支持数组以容纳新数据时的变异方式来改变切片。有两种常见的处理方式。
通过返回新的切片:
func addWindow(windows []Window) []Window {
return append(windows, Window{1, 1})
}
room.Windows = addWindow(room.Windows)
或者通过提供一个可变参数,调用方保持对其的引用:
func addWindow(room *Room) {
room.Windows = append(room.Windows, Window{1, 1})
}
对于第二个问题,你必须确保值不会以不安全的方式同时发生变异。也有许多方法来解决这个问题:
使用通道
不直接操作房间,而是要求窗口由N个goroutine生成,并将它们的结果报告回一个非竞争的控制点。例如,你可以这样做:
windows := make(chan Window, N)
for i := 0; i < N; i++ {
go createWindow(windows)
}
for i := 0; i < N; i++ {
room.Windows = append(room.Windows, <-windows)
}
addWindow
看起来会类似于:
func createWindow(windows chan Window) {
windows <- Window{1, 1}
}
这样创建是并发的,但实际的房间操作不是。
添加互斥锁字段
通常在类型本身中有一个私有的互斥锁字段,例如:
type Room struct {
m sync.Mutex
Windows []Window
}
然后,每当操作并发敏感字段时,使用互斥锁保护独占区域:
room.m.Lock()
room.Windows = append(room.Windows, window)
room.m.Unlock()
理想情况下,使用这样的互斥锁应该保持封装在类型本身附近,这样很容易看到它是如何使用的。因此,你经常会看到互斥锁从类型本身的方法中使用(例如 room.addWindow
)。
如果在独占(受保护)区域中存在可能引发 panic 的逻辑,最好在 Lock
之后立即延迟 Unlock
调用。很多人只是将它们一个接一个地放置,即使在简单的操作中也是如此,这样他们就不必弄清楚是否安全执行。如果你不确定,这可能是一个好主意。
非常重要:大多数情况下,通过值复制带有互斥锁字段的结构体是一个坏主意。相反,使用指向原始值的指针。原因是互斥锁在内部依赖于其字段的地址不变,以使原子操作正常工作。
添加全局互斥锁
在更不寻常的情况下,这可能不适用于你要处理的情况,但了解一下也是好的,你可以选择保护逻辑本身而不是保护数据。一种方法是使用全局互斥锁变量,大致如下:
var addWindowMutex sync.Mutex
func addWindow(room *Room) {
addWindowMutex.Lock()
room.Windows = append(room.Windows, Window{1, 1})
addWindowMutex.Unlock()
}
这样,无论谁调用 addWindow
,它本身都受到保护。这种方法的优点是你不依赖于房间的实现来实现它。缺点是只有一个goroutine能进入独占区域,无论有多少个房间在并行处理(这在前一个解决方案中不是这种情况)。
在这样做时,请记住,读取 room.Windows
或任何在独占区域中发生变异的数据也应该受到保护,以防同时进行更改。
最后,作为一些未经提示的反馈,请检查那些错误值。忽略错误是一个非常糟糕的做法,无论是一个示例还是严肃的代码。很多时候,即使在构建这样的示例代码时,你也会捕获到错误。
英文:
There are two different issues in your logic: the first one is how the slice itself is being manipulated, and the second one regards actual concurrency problems.
For the slice manipulation, simply passing the slice by value as a parameter will mean that you won't be able to mutate the slice in a way that the call site will see it when the slice has to be grown or the backing array reallocated to accommodate the new data you're appending. There are two common ways to handle that.
By returning the new slice:
func addWindow(windows []Window) []Window {
return append(windows, Window{1, 1})
}
room.Windows = addWindow(room.Windows)
Or by providing a mutable parameter that the call site maintains a reference to:
func addWindow(room *Room) {
room.Windows = append(room.Windows, Window{1, 1})
}
For the second issue, you must make sure values are not being mutated concurrently in an unsafe way. There are many ways to address it as well:
Use a channel
Instead of a manipulating the room directly, you can ask windows to be produced by N goroutines, and have their results reported back to a non-racy control point. For example, you might have:
windows := make(chan Window, N)
for i := 0; i < N; i++ {
go createWindow(windows)
}
for i := 0; i < N; i++ {
room.Windows = append(room.Windows, <-windows)
}
and addWindow
would instead look similar to:
func createWindow(windows chan Window) {
windows <- Window{1, 1}
}
This way the creation is concurrent, but the actual manipulation of the room is not.
Add a mutex field
It's also typical to have a private mutex field in the type itself, such as:
type Room struct {
m sync.Mutex
Windows []Window
}
Then, whenever manipulating concurrency-sensitive fields, protect the exclusive area with the mutex:
room.m.Lock()
room.Windows = append(room.Windows, window)
room.m.Unlock()
Ideally the use of such a mutex should stay encapsulated close to the type itself, so it's easy to spot how it's being used. For that reason, you'll often see the mutex being used from within methods of the type itself (room.addWindow
, for example).
If you have panic-prone logic in the exclusive (protected) region, it may be a good idea to defer the Unlock
call right after the Lock
one. A lot of people simply put one straight after the other, even in simple operations, just so they don't have to figure whether it's safe or not to do so. That may well be a good idea if you're unsure.
VERY IMPORTANT: In most cases it's a bad idea to copy a struct with a mutex field by value. Instead, use a pointer to the original value. The reason for this is that internally the mutex relies on the address of its fields to not change for the atomic operations to work correctly.
Add a global mutex
In more unusual circumstances, which most probably do not apply for the case you're trying to handle, but which is good knowing about, you may choose to protect the logic itself instead of protecting the data. One way to do that is with a global mutex variable, with something around the lines of:
var addWindowMutex sync.Mutex
func addWindow(room *Room) {
addWindowMutex.Lock()
room.Windows = append(room.Windows, Window{1, 1})
addWindowMutex.Unlock()
}
This way addWindow
itself is protected, no matter who is calling it. The advantage of that approach is that you don't depend on the implementation of room to do it. A disadvantage is that only a single goroutine will get into the exclusive region, no matter how many rooms are being processed in parallel (that's not the case with the prior solution).
When doing this, remember that reading room.Windows
or whatever data is being mutated in the exclusive region should also be protected, in case there's still concurrency going on to change it meanwhile.
Finally, just as some unprompted feedback, do check those error values. Ignoring errors is a really bad practice, whether it's just an example or serious code. Many times you'll catch errors even when building up sample code like that.
答案2
得分: 0
package main
import (
"encoding/json"
"fmt"
"sync"
)
type Window struct {
Height int64 `json:"Height"`
Width int64 `json:"Width"`
}
type Room struct {
mu sync.Mutex
Windows []Window `json:"Windows"`
}
func main() {
js := []byte(`{"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20}]}`)
fmt.Printf("应该有2个窗户:%v\n", string(js))
var room Room
_ = json.Unmarshal(js, &room)
var wg sync.WaitGroup
// 向房间添加多个窗户
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
addWindow(&room)
}()
}
wg.Wait()
js, _ = json.Marshal(room)
fmt.Printf("应该有12个窗户:%v\n", string(js))
}
func addWindow(r *Room) {
window := Window{1, 1}
fmt.Printf("向%v添加%v\n", r.Windows, window)
r.mu.Lock()
defer r.mu.Unlock()
r.Windows = append(r.Windows, window)
}
应该有2个窗户:{"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20}]}
向[{10 20} {10 20}]添加{1 1}
向[{10 20} {10 20} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1} {1 1} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]添加{1 1}
向[{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]添加{1 1}
应该有12个窗户:{"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1}]}
<details>
<summary>英文:</summary>
package main
import (
"encoding/json"
"fmt"
"sync"
)
type Window struct {
Height int64 `json:"Height"`
Width int64 `json:"Width"`
}
type Room struct {
mu sync.Mutex
Windows []Window `json:"Windows"`
}
func main() {
js := []byte(`{"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20}]}`)
fmt.Printf("Should have 2 windows: %v\n", string(js))
var room Room
_ = json.Unmarshal(js, &room)
var wg sync.WaitGroup
// Add meny windows to room
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
addWindow(&room)
}()
}
wg.Wait()
js, _ = json.Marshal(room)
fmt.Printf("Sould have 12 windows: %v\n", string(js))
}
func addWindow(r *Room) {
window := Window{1, 1}
fmt.Printf("Adding %v to %v\n", window, r.Windows)
r.mu.Lock()
defer r.mu.Unlock()
r.Windows = append(r.Windows, window)
}
Should have 2 windows: {"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20}]}
Adding {1 1} to [{10 20} {10 20}]
Adding {1 1} to [{10 20} {10 20} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1} {1 1} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]
Adding {1 1} to [{10 20} {10 20} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1} {1 1}]
Sould have 12 windows: {"Windows":[{"Height":10,"Width":20},{"Height":10,"Width":20},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1},{"Height":1,"Width":1}]}
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论