您正在使用
for带有
range表达式的语句。引用规范:对于语句:
范围表达式在开始循环之前先进行一次评估
,但有一个例外:如果范围表达式是一个数组或一个指向数组的指针,并且最多存在一个迭代变量,则仅评估范围表达式的长度;否则,不执行任何 *** 作。如果该长度是常数,则根据定义,范围表达式本身将不会被求值。
我们在地图上进行范围调整,因此也不例外:范围表达式在开始循环之前仅被评估一次。范围表达式只是一个map变量
testMap:
for k, _ := range testMap {}
映射值不包括键值对,它仅 指向
包含键值对的数据结构。为什么这很重要?由于映射值仅被评估一次,并且如果以后添加了对,则在循环之前评估一次的映射值将成为仍然指向包含这些新对的数据结构的映射。这与对切片(也将被评估一次)的范围形成对比,切片也只是指向包含元素的后备数组的标头;但是如果在迭代过程中将元素添加到切片中,
_即使_如果那不会导致分配并复制到新的后备数组,则它们将不包含在迭代中(因为slice头还包含已评估的长度)。将元素追加到切片可能会产生新的切片值,但是将对添加到地图不会导致新的地图值。
现在开始迭代:
for k, v := range testMap { t1 := time.Now() someFunction() t2 := time.Now()}
在进入块之前,
t1 := time.Now()在行
k和
v变量保存迭代值之前,它们 已经 从映射中 读出
(否则它们无法保存值)。问:你认为地图是由读
for ... range语句 之间
t1和
t2?在什么情况下会发生这种情况?我们这里有
一个 正在执行的goroutine
someFunc()。为了能够通过该语句 访问 地图
for,这可能需要 另一个
goroutine或将要 暂停
someFunc()。显然,这些都没有发生。(
for ...range结构不是多够程的怪物。)不管有多少次迭代有, 而
someFunc()被执行时,地图不被访问的
for声明。
因此,要回答您的一个问题:
for在执行迭代时,不会在块内访问该映射,但是在为下一个迭代设置
k和
v值(已分配)时会访问该映射。这意味着在地图上
进行 以下迭代 对于并发访问 是 安全的 :
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()(必须)应作为延迟的语句进行,因为在原始代码中,您可能会返回“early”并出现错误,在这种情况下您没有解锁,这意味着地图保持锁定状态!(在此处建模
if someCond {...})。
还要注意,这种类型的锁定仅在并发访问的情况下才能确保锁定。 它不会阻止并发goroutine修改(例如添加新对)映射。
修改(如果使用写锁适当地保护)将是安全的,并且循环可以继续,但是不能保证for循环将在新对上进行迭代:
如果在迭代过程中删除尚未到达的映射条目,则不会生成相应的迭代值。如果映射条目是在迭代过程中创建的,则该条目可能在迭代过程中产生或可以被跳过。对于创建的每个条目以及从一个迭代到下一个迭代,选择可能有所不同。
写锁定保护的修改可能如下所示:
func WriteTestMap(k, v int) { testMapLock.Lock() defer testMapLock.Unlock() testMap[k] = v}
现在,如果您在的代码块中释放了读取锁,则
for并发的goroutine可以自由地获取写入锁并对地图进行修改。在您的代码中:
testMapLock <- trueiteratorChannel <- k<-testMapLock
在上发送
k时
iteratorChannel,并发goroutine可能会修改地图。这不仅是一种“不幸”的情况,在通道上发送值通常是“阻塞” *** 作,如果通道的缓冲区已满,则必须准备好接收另一个goroutine才能使发送 *** 作继续进行。在通道上发送值是运行时甚至在同一OS线程上运行其他goroutine的良好调度点,更不用说是否有多个OS线程,其中一个OS线程可能已经在“等待”顺序进行写锁定进行地图修改。
总结一下最后一部分:释放
for块内的读取锁就像对别人大喊大叫:“来吧,如果您敢,请立即修改地图!” 因此,在您的代码中
mySeq !=testMapSequence很可能会遇到这种情况。请参见以下可运行示例进行演示(它是您的示例的变体):
package mainimport ( "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 24concurrent modification 41concurrent modification 463concurrent modification 477concurrent modification 482concurrent modification 496concurrent modification 508concurrent modification 521concurrent modification 525concurrent modification 535concurrent modification 541concurrent modification 555concurrent modification 561concurrent modification 565concurrent modification 570concurrent modification 577concurrent modification 591concurrent modification 593
我们经常遇到并发修改!
您要避免这种并发修改吗?解决方案非常简单:请勿释放中的读取锁
for。另外,运行您的应用程序并
-race选择检测竞争状况的选项:
go run-race testmap.go
最后的想法
语言规范清楚地允许您 在同一goroutine 范围内修改地图,这就是前面的引用所涉及的内容(
“如果在迭代过程中删除了尚未到达的地图条目…。在迭代过程中创建…”
)。允许在同一goroutine中修改地图,这是安全的,但未定义迭代器逻辑如何处理地图。
如果在另一个goroutine中修改了映射,如果您使用适当的同步,则Go Memory
Model会保证带有的goroutine
for ...range将遵守所有修改,并且迭代器逻辑将看到它,就好像“自己的” goroutine将对其进行修改一样–如前所述,这是允许的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)