Golang的映射(maps)在并发读写操作方面有多安全?

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

How safe are Golang maps for concurrent Read/Write operations?

问题

根据Go博客的说法,

Map在并发使用时是不安全的:当同时读取和写入Map时,未定义会发生什么。如果你需要在并发执行的goroutine中从Map中读取和写入数据,访问必须通过某种同步机制进行调节。
(来源:https://blog.golang.org/go-maps-in-action)

有人可以详细解释一下吗?并发读操作在不同的例程中似乎是允许的,但是如果尝试从同一个键读取和写入数据,可能会产生竞态条件。

在某些情况下,可以减少这种风险吗?例如:

  • 函数A生成k并将m[k]=0。这是A唯一一次写入Map m。已知k不在m中。
  • A将k传递给并发运行的函数B。
  • A然后读取m[k]。如果m[k]==0,它会等待,只有当m[k]!=0时才继续。
  • B在Map中查找k。如果找到了,B将m[k]设置为某个正整数。如果没有找到,B将等待直到k在m中出现。

这不是代码(显然),但我认为它展示了一个情况的轮廓,即使A和B都尝试访问m,也不会产生竞态条件,或者如果有竞态条件,由于额外的约束也不会产生影响。

英文:

According to the Go blog,

>Maps are not safe for concurrent use: it's not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated by some kind of synchronization mechanism.
(source: https://blog.golang.org/go-maps-in-action)

Can anyone elaborate on this? Concurrent read operations seem permissible across routines, but concurrent read/write operations may generate a race condition if one attempts to read from and write to the same key.

Can this last risk be reduced in some cases? For example:

  • Function A generates k and sets m[k]=0. This is the only time A writes to map m. k is known to not be in m.
  • A passes k to function B running concurrently
  • A then reads m[k]. If m[k]==0, it waits, continuing only when m[k]!=0
  • B looks for k in the map. If it finds it, B sets m[k] to some positive integer. If it doesn't it waits until k is in m.

This isn't code (obviously) but I think it shows the outlines of a case where even if A and B both try to access m there won't be a race condition, or if there is it won't matter because of the additional constraints.

答案1

得分: 118

在Golang 1.6之前,可以并发读取,但不能并发写入,但是写入和并发读取是可以的。自从Golang 1.6以后,在写入map时不能读取map。
因此,在Golang 1.6之后,对并发访问map应该像这样:

package main

import (
	"sync"
	"time"
)

var m = map[string]int{"a": 1}
var lock = sync.RWMutex{}

func main() {
	go Read()
	time.Sleep(1 * time.Second)
	go Write()
	time.Sleep(1 * time.Minute)
}

func Read() {
	for {
		read()
	}
}

func Write() {
	for {
		write()
	}
}

func read() {
	lock.RLock()
	defer lock.RUnlock()
	_ = m["a"]
}

func write() {
	lock.Lock()
	defer lock.Unlock()
	m["b"] = 2
}

否则,你将会得到下面的错误:
Golang的映射(maps)在并发读写操作方面有多安全?

添加:

你可以使用go run -race race.go来检测竞争条件。

修改read函数:

func read() {
	// lock.RLock()
	// defer lock.RUnlock()
	_ = m["a"]
}

Golang的映射(maps)在并发读写操作方面有多安全?

另一种选择:

众所周知,map是由桶实现的,而sync.RWMutex将锁定所有的桶。concurrent-map使用fnv32来分片键,并且每个桶使用一个sync.RWMutex

英文:

Before Golang 1.6, concurrent read is OK, concurrent write is not OK, but write and concurrent read is OK. Since Golang 1.6, map cannot be read when it's being written.
So After Golang 1.6, concurrent access map should be like:

package main

import (
	"sync"
	"time"
)

var m = map[string]int{"a": 1}
var lock = sync.RWMutex{}

func main() {
	go Read()
	time.Sleep(1 * time.Second)
	go Write()
	time.Sleep(1 * time.Minute)
}

func Read() {
	for {
		read()
	}
}

func Write() {
	for {
		write()
	}
}

func read() {
	lock.RLock()
	defer lock.RUnlock()
	_ = m["a"]
}

func write() {
	lock.Lock()
	defer lock.Unlock()
	m["b"] = 2
}

Or you will get the error below:
Golang的映射(maps)在并发读写操作方面有多安全?

ADDED:

You can detect the race by using go run -race race.go

Change the read function:

func read() {
	// lock.RLock()
	// defer lock.RUnlock()
	_ = m["a"]
}

Golang的映射(maps)在并发读写操作方面有多安全?

Another choise:

As we known, map was implemented by buckets and sync.RWMutex will lock all the buckets. concurrent-map use fnv32 to shard the key and every bucket use one sync.RWMutex.

答案2

得分: 22

并发读取(只读)是可以的。并发写入和/或读取是不可以的。

如果访问是同步的,例如通过sync包、通道或其他方式,多个goroutine才能写入和/或读取同一个映射。

你的例子:

> 1. 函数A生成k并将m[k]=0。这是A写入映射m的唯一时机。已知k不在m中。
> 2. A将k传递给并发运行的函数B。
> 3. A然后读取m[k]。如果m[k]==0,它会等待,直到m[k]!=0才继续。
> 4. B在映射中查找k。如果找到了,B将m[k]设置为某个正整数。如果没有找到,B会等待直到k在m中出现。

你的例子有两个goroutine:A和B,在步骤3中A尝试读取m,在步骤4中B尝试写入它。没有同步(你没有提到任何同步),所以这本身是不允许的/不确定的。

这是什么意思?不确定意味着即使B写入了m,A可能永远不会观察到这个变化。或者A可能观察到一个根本没有发生的变化。或者可能会发生恐慌。或者地球可能因为这种非同步的并发访问而爆炸(尽管这种情况的几率非常小,甚至可能小于1e-40)。

相关问题:

https://stackoverflow.com/questions/11063473/map-with-concurrent-access

https://stackoverflow.com/questions/31730684/what-does-not-being-thread-safe-means-about-maps-in-go

https://stackoverflow.com/questions/35431102/what-is-the-danger-of-neglecting-goroutine-thread-safety-when-using-a-map-in-go

英文:

Concurrent read (read only) is ok. Concurrent write and/or read is not ok.

Multiple goroutines can only write and/or read the same map if access is synchronized, e.g. via the sync package, with channels or via other means.

Your example:

> 1. Function A generates k and sets m[k]=0. This is the only time A writes to map m. k is known to not be in m.
> 2. A passes k to function B running concurrently
> 3. A then reads m[k]. If m[k]==0, it waits, continuing only when m[k]!=0
> 4. B looks for k in the map. If it finds it, B sets m[k] to some positive integer. If it doesn't it waits until k is in m.

Your example has 2 goroutines: A and B, and A tries to read m (in step 3) and B tries to write it (in step 4) concurrently. There is no synchronization (you didn't mention any), so this alone is not permitted / not determined.

What does it mean? Not determined means even though B writes m, A may never observe the change. Or A may observe a change that didn't even happen. Or a panic may occur. Or the Earth may explode due to this non-synchronized concurrent access (although the chance of this latter case is extremely small, maybe even less than 1e-40).

Related questions:

https://stackoverflow.com/questions/11063473/map-with-concurrent-access

https://stackoverflow.com/questions/31730684/what-does-not-being-thread-safe-means-about-maps-in-go

https://stackoverflow.com/questions/35431102/what-is-the-danger-of-neglecting-goroutine-thread-safety-when-using-a-map-in-go

答案3

得分: 16

> Go 1.6 发布说明
>
> 运行时已经添加了轻量级、尽力检测并发地错误使用映射的功能。与往常一样,如果一个 goroutine 正在写入映射,其他 goroutine 就不应该同时读取或写入该映射。如果运行时检测到这种情况,它会打印诊断信息并使程序崩溃。了解更多有关该问题的信息的最佳方法是在竞争检测器下运行程序,它将更可靠地识别竞争并提供更多细节。

映射是复杂的、自我重组的数据结构。并发读写访问是未定义的。

没有代码,没有其他可以说的了。

英文:

> Go 1.6 Release Notes
>
> The runtime has added lightweight, best-effort detection of concurrent
> misuse of maps. As always, if one goroutine is writing to a map, no
> other goroutine should be reading or writing the map concurrently. If
> the runtime detects this condition, it prints a diagnosis and crashes
> the program. The best way to find out more about the problem is to run
> the program under the race detector, which will more reliably identify
> the race and give more detail.

Maps are complex, self-reorganizing data structures. Concurrent read and write access is undefined.

Without code, there's not much else to say.

答案4

得分: 5

经过长时间的讨论,决定了地图的典型用法不需要从多个goroutine进行安全访问,在那些需要安全访问的情况下,地图可能是某个较大的数据结构或计算的一部分,而这些已经被同步。因此,要求所有地图操作都获取互斥锁会减慢大多数程序的速度,并且只对少数程序增加安全性。然而,这并不是一个容易的决定,因为这意味着不受控制的地图访问可能会导致程序崩溃。

语言并不排除原子地图更新的可能性。当需要时,例如在托管不受信任的程序时,实现可以交错地图访问。

只有在进行更新时,地图访问才是不安全的。只要所有的goroutine只是读取地图中的元素,包括使用for range循环进行迭代,而不是通过赋值给元素或进行删除来更改地图,它们可以安全地并发访问地图而无需同步。

为了帮助正确使用地图,语言的一些实现包含了一个特殊的检查,当地图被并发执行不安全地修改时,它会在运行时自动报告。

英文:

After long discussion it was decided that the typical use of maps did not require safe access from multiple goroutines, and in those cases where it did, the map was probably part of some larger data structure or computation that was already synchronized. Therefore requiring that all map operations grab a mutex would slow down most programs and add safety to few. This was not an easy decision, however, since it means uncontrolled map access can crash the program.

The language does not preclude atomic map updates. When required, such as when hosting an untrusted program, the implementation could interlock map access.

Map access is unsafe only when updates are occurring. As long as all goroutines are only reading—looking up elements in the map, including iterating through it using a for range loop—and not changing the map by assigning to elements or doing deletions, it is safe for them to access the map concurrently without synchronization.

As an aid to correct map use, some implementations of the language contain a special check that automatically reports at run time when a map is modified unsafely by concurrent execution.

答案5

得分: 4

你可以使用sync.Map,它可以安全地进行并发使用。唯一的注意事项是你将放弃类型安全,并将所有对你的映射的读写更改为使用此类型定义的方法。

英文:

You can use sync.Map which is safe for concurrent use. The only caveat is that you are gonna give up on type safety and change all the reads and writes to your map to use the methods defined for this type

答案6

得分: 1

你可以在map中存储一个指向int的指针,并且可以有多个goroutine读取指向的int,而另一个goroutine则写入新值到该int。在这种情况下,map并没有被更新。

这种做法在Go语言中并不常见,也不是你所询问的内容。

或者,你可以传递一个索引给数组,让一个goroutine更新该位置,而其他goroutine则读取该位置。

但是你可能只是想知道为什么当map中已经存在某个键时,不能更新其对应的值。可以推测的是,map的哈希方案并没有被改变,至少在当前的实现中是如此。Go语言的作者似乎不希望为这种特殊情况做出特殊的处理。通常他们希望代码易于阅读和理解,而像不允许在其他goroutine可能正在读取时进行map写入这样的规则可以保持事情简单。而且从1.6版本开始,它们甚至可以在正常运行时捕获到误用,从而节省了许多人的调试时间。

英文:

You can store a pointer to an int in the map, and have multiple goroutines read the int being pointed to while another writes a new value to the int. The map is not being updated in this case.

This wouldn't be idiomatic for Go and not what you were asking.

Or instead of passing a key to a map, you could pass the index to an array, and have that updated by one goroutine while others read the location.

But you're probably just wondering why a map's value can't be updated with a new value when the key is already in the map. Presumably nothing about the map's hashing scheme is being changed - at least not given their current implementation. It would seem the Go authors don't want to make allowances for such special cases. Generally they want code to be easy to read and understand, and a rule like not allowing map writes when other goroutines could be reading keeps things simple and now in 1.6 they can even start to catch misuse during normal runtimes - saving many people many hours of debugging.

答案7

得分: 1

正如这里的其他答案所述,原生的map类型不是goroutine安全的。在阅读当前答案后,有几点需要注意:

  1. 不要使用defer来解锁,它会对性能产生一些影响(参见这篇很好的文章)。直接调用解锁操作。
  2. 通过减少锁之间的时间来提高性能。例如,通过对map进行分片。
  3. 有一个常用的包(在GitHub上有接近400个星标)可以解决这个问题,叫做concurrent-map在这里可以找到,它考虑了性能和易用性。你可以使用它来处理并发问题。
英文:

As the other answers here stated, the native map type is not goroutine-safe. A couple of notes after reading the current answers:

  1. Do not use defer to unlock, it has some overhead that affects performance (see this nice post). Call unlock directly.
  2. You can achieve better performance by reducing time spent between locks. For example, by sharding the map.
  3. There is a common package (approaching 400 stars on GitHub) used to solve this called concurrent-map here which has performance and usability in mind. You could use it to handle the concurrency issues for you.

答案8

得分: 0

在Golang中,对于只读操作,Map是并发安全的。假设你的Map在写入后不再被写入,那么你不需要任何互斥锁来确保只有一个goroutine访问你的Map。下面是一个关于Map并发安全读取的示例:

package main

import (
	"fmt"
	"sync"
)

var freq map[int]int

// 并发从Map中读取的示例
func main() {
	// 在从goroutine访问之前,Map已经被写入
	freq = make(map[int]int)
	freq[1] = 1
	freq[2] = 2

	wg := sync.WaitGroup{}
	wg.Add(10)

	for i := 1; i <= 10; i++ {
		// 在goroutine中,我们只从Map中读取值
		go func(id int, loop int) {
			defer wg.Done()
			fmt.Println("在循环中", loop)
			fmt.Println("1的频率:", freq[id])
		}(1, i)
	}

	wg.Wait()
}

请注意,我只翻译了代码部分,其他内容不做翻译。

英文:

Map is concurrent safe for read only in Golang. Let's say, your map is written first and never be written again then you don't need any mutex type of thing to make sure that only one go routine is accessing your map. I have given an example below about map concurrent safe reading.

package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

var freq map[int]int

// An example of concurrent read from a map
func main()  {
	// Map is written before accessing from go routines
	freq = make(map[int]int)
	freq[1] = 1
	freq[2] = 2

	wg := sync.WaitGroup{}
	wg.Add(10)

	for i:=1;i&lt;=10;i++ {
		// In go routine we are only reading val from map
		go func(id int, loop int) {
			defer wg.Done()
			fmt.Println(&quot;In loop &quot;, loop)
			fmt.Println(&quot;Freq of 1: &quot;, freq[id])
		}(1, i)
	}

	wg.Wait()
}

huangapple
  • 本文由 发表于 2016年3月23日 07:35:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/36167200.html
匿名

发表评论

匿名网友

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

确定