您正在使用带有range
表达式的for
语句.引述自Spec: For statements:
The range expression is evaluated once before beginning the loop,但有一个例外:如果范围表达式是数组或指向数组的指针,且最多存在一个迭代变量,则只计算范围表达式的长度;如果该长度为常数,则不会计算范围表达式本身.
我们在 map 上进行范围调整,因此也不例外:在开始循环之前,范围表达式只计算一次.范围表达式是简单的映射变量testMap
:
for k, _ := range testMap {}
映射值不包括键-值对,它只有points到包含键-值对的数据 struct .为什么这很重要?因为映射值只判断一次,并且如果后来的对被添加到映射中,那么映射值(在循环之前判断一次)将是仍然指向包括那些新对的数据 struct 的映射.这与在片段上排列(这也将被判断一次)形成对比,该片段也只是指向保存元素的后备数组的头部;但是如果在迭代期间将元素添加到片段,even如果这不会导致分配和复制到新的后备数组,则它们将不会被包括在迭代中(因为片段头部还包含已经被判断的长度).将元素追加到切片可能会产生新的切片值,但向映射添加对不会产生新的映射值.
现在开始迭代:
for k, v := range testMap {
t1 := time.Now()
someFunction()
t2 := time.Now()
}
在我们进入块之前,在t1 := time.Now()
行k
和v
变量保存迭代的值之前,它们是 map 上的already read out(否则它们无法保存值).问题:你认为 map 是由for ... range
语句between t1
和t2
读取的吗?在什么情况下会发生这种情况?我们这里有一个single goroutine,执行someFunc()
.要想用for
语句来绘制 map ,要么需要another个goroutine,要么需要suspend个someFunc()
.显然这两种情况都没有发生.(for ... range
构造不是一个多goroutine怪物.)不管有多少次迭代,while 106 is executed, the map is not accessed by the 107 statement次.
因此,我来回答您的一个问题:在执行迭代时,不会在for
%的挡路内访问该映射,但是在为下一个迭代设置(分配)k
时会访问该映射.这意味着 map 上的以下迭代为safe for concurrent access:
var (
testMap = make(map[int]int)
testMapLock = &sync.RWMutex{}
)
func IterateMapKeys(iteratorChannel chan int) error {
testMapLock.RLock()
defer testMapLock.RUnlock()
for k, v := range testMap {
testMapLock.RUnlock()
someFunc()
testMapLock.RLock()
if someCond {
return someErr
}
}
return nil
}
请注意,在IterateMapKeys()
中解锁应该(必须)作为延迟语句发生,因为在原始代码中,您可能会"提前"返回一个错误,在这种情况下,您没有解锁,这意味着 map 仍然处于锁定状态!(此处由if someCond {...}
建模).
还要注意,这种类型的锁定仅在并发访问的情况下确保锁定.It does not prevent a concurrent goroutine to modify (e.g. add a new pair) the map.修改(如果使用写锁适当保护)将是安全的,循环可以继续,但不能保证for循环将在新的对上迭代:
如果在迭代过程中删除了尚未到达的映射条目,则不会生成相应的迭代值.如果在迭代期间创建映射条目,则该条目可能在迭代期间生成,也可能被跳过.对于创建的每个条目,以及从一个迭代到下一个迭代, Select 可能会有所不同.
写锁保护的修改可能如下所示:
func WriteTestMap(k, v int) {
testMapLock.Lock()
defer testMapLock.Unlock()
testMap[k] = v
}
现在,如果释放for
块中的读锁,并发goroutine可以自由地获取写锁并修改映射.在代码中:
testMapLock <- true
iteratorChannel <- k
<-testMapLock
在iteratorChannel
上发送k
时,并发goroutine可能会修改映射.这不仅仅是一种"不吉利"的情况,在通道上发送值通常是一种"阻塞"操作,如果通道的缓冲区已满,则必须准备好另一个goroutine来接收,才能继续发送操作.在通道上发送值对于运行时来说是一个很好的调度点,可以在同一个OS线程上运行其他Goroutine,更不用说是否有多个OS线程,其中一个可能已经在"等待"写锁以执行映射修改.
总结最后一部分:你在for
块中释放读锁就像对其他人大喊:"来吧,如果你敢,现在就修改 map !"因此,在代码中很可能会遇到mySeq != testMapSequence
.请看这个可运行的示例来演示它(这是您示例的一个变体):
package main
import (
"fmt"
"math/rand"
"sync"
)
var (
testMap = make(map[int]int)
testMapLock = &sync.RWMutex{}
testMapSequence int
)
func main() {
go func() {
for {
k := rand.Intn(10000)
WriteTestMap(k, 1)
}
}()
ic := make(chan int)
go func() {
for _ = range ic {
}
}()
for {
if err := IterateMapKeys(ic); err != nil {
fmt.Println(err)
}
}
}
func WriteTestMap(k, v int) {
testMapLock.Lock()
defer testMapLock.Unlock()
testMap[k] = v
testMapSequence++
}
func IterateMapKeys(iteratorChannel chan int) error {
testMapLock.RLock()
defer testMapLock.RUnlock()
mySeq := testMapSequence
for k, _ := range testMap {
testMapLock.RUnlock()
iteratorChannel <- k
testMapLock.RLock()
if mySeq != testMapSequence {
//close(iteratorChannel)
return fmt.Errorf("concurrent modification %d", testMapSequence)
}
}
return nil
}
示例输出:
concurrent modification 24
concurrent modification 41
concurrent modification 463
concurrent modification 477
concurrent modification 482
concurrent modification 496
concurrent modification 508
concurrent modification 521
concurrent modification 525
concurrent modification 535
concurrent modification 541
concurrent modification 555
concurrent modification 561
concurrent modification 565
concurrent modification 570
concurrent modification 577
concurrent modification 591
concurrent modification 593
我们经常遇到并发修改!
是否要避免这种并发修改?解决方案非常简单:不要释放for
内部的读锁定.还可以使用-race
选项运行您的应用程序,以检测竞态条件:go run -race testmap.go
Final thoughts
语言规范清楚地允许您在调整 map in the same goroutine时对其进行修改,这就是前面引用的内容("If map entries that have not yet been reached are removed during iteration.... If map entries are created during iteration...").修改同一Goroutine中的映射是允许的,也是安全的,但是迭代器逻辑如何处理它并没有定义.
如果映射在另一个goroutine中被修改,如果您使用了正确的同步,The Go Memory Model将保证for ... range
的goroutine将观察到所有修改,迭代器逻辑将看到它,就像"它自己的"goroutine会修改它一样——这是允许的,如前所述.