在Go中同时访问带有'range'的地图

时间:2016-11-05 20:25:35

标签: for-loop dictionary go foreach concurrency

Go博客中的“Go maps in action”条目指出:

  

映射对于并发使用是不安全的:它没有定义当您同时读取和写入时会发生什么。如果您需要从同时执行的goroutine中读取和写入映射,则访问必须由某种同步机制调解。保护地图的一种常用方法是使用sync.RWMutex。

但是,访问地图的一种常用方法是使用range关键字对其进行迭代。目前尚不清楚,为了并发访问的目的,range循环内的执行是“读”,还是只是该循环的“周转”阶段。例如,以下代码可能会或可能不会与“地图上没有并发r / w”规则相冲突,具体取决于range操作的特定语义/实现:

 var testMap map[int]int
 testMapLock := make(chan bool, 1)
 testMapLock <- true
 testMapSequence := 0

...

 func WriteTestMap(k, v int) {
    <-testMapLock
    testMap[k] = v
    testMapSequence++
    testMapLock<-true
 }

 func IterateMapKeys(iteratorChannel chan int) error {
    <-testMapLock
    defer func() { 
       testMapLock <- true
    }
    mySeq := testMapSequence
    for k, _ := range testMap {
       testMapLock <- true
       iteratorChannel <- k
       <-testMapLock
       if mySeq != testMapSequence {
           close(iteratorChannel)
           return errors.New("concurrent modification")
       }
    }
    return nil
 }

这里的想法是当第二个函数等待使用者获取下一个值时,range“迭代器”打开,并且当时编写器未被阻止。但是,从来没有一个迭代器中的两个读取位于写入的任何一侧 - 这是一个“快速失败”的迭代器,借用Java术语。

语言规范或其他文档中是否有任何内容表明这是否合法?我可以看到它走向任何一种方式,上面引用的文件并不清楚究竟是什么构成了“阅读”。文档在for / range语句的并发方面似乎完全安静。

(请注意这个问题是关于for/range的货币,但不是重复:Golang concurrent map access with range - 用例完全不同,我问的是'范围内的精确锁定要求'关键字在这里!)

2 个答案:

答案 0 :(得分:7)

您正在使用带有for表达式的range语句。引自Spec: For statements:

  

范围表达式在开始循环之前计算一次,但有一个例外:如果范围表达式是数组或指向数组的指针,并且最多只存在一个迭代变量,则只有范围表达式的长度进行评估;如果该长度是常数,by definition将不会评估范围表达式本身。

我们在地图上进行测距,所以它不是例外:在开始循环之前,范围表达式仅被评估一次。范围表达式只是一个映射变量testMap

for k, _ := range testMap {}

地图值不包含键值对,只有指向到数据结构。为什么这很重要?因为地图值仅被评估一次,并且如果稍后的对被添加到地图中,则在循环之前评估一次的地图值将是仍然指向包括那些新对的数据结构的地图。这与在切片上进行测量(也将被评估一次)形成对比,该切片也仅是指向保持元素的后备阵列的标题;但是如果在迭代期间将元素添加到切片中,甚至如果这不会导致分配并复制到新的后备数组,则它们将不会包含在迭代中(因为切片头也是包含长度 - 已经评估过)。将元素附加到切片可能会产生新的切片值,但将对添加到地图将不会产生新的地图值。

现在进行迭代:

for k, v := range testMap {
    t1 := time.Now()
    someFunction()
    t2 := time.Now()
}

在我们进入该区块之前,在t1 := time.Now()kv变量保持迭代值之前,它们已经读出从地图(否则他们无法保持价值)。问题:您认为地图是由 for ... ranget1之间的t2声明读取的吗?在什么情况下会发生这种情况?我们这里有一个 goroutine正在执行someFunc()。为了能够通过for语句访问地图,这需要另一个 goroutine,或者需要暂停 someFunc()。显然,这些都不会发生。 (for ... range构造不是多goroutine怪物。)无论有多少次迭代,执行someFunc()for语句都不会访问该地图

所以回答你的一个问题:执行迭代时不会在for块内访问映射,但是在设置kv值时访问它(已分配)用于下一次迭代。这意味着对映射的以下迭代对于并发访问是安全的

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()中的解锁应该(必须)作为延迟声明发生,就像在您的原始代码中一样,您可以返回&#34;早期&#34;有错误,在这种情况下你没有解锁,这意味着地图仍然锁定! (此处由if someCond {...}建模)。

另请注意,此类锁定仅在并发访问时确保锁定。 它不会阻止并发goroutine修改(例如添加新对)映射。修改(如果使用写锁定进行适当保护)将是安全的,并且循环可能会继续,但是没有保证for循环将迭代新对:

  

如果在迭代期间删除了尚未到达的映射条目,则不会生成相应的迭代值。如果在迭代期间创建了映射条目,则可以在迭代期间生成该条目,或者可以跳过该条目。对于每个创建的条目以及从一次迭代到下一次迭代,选择可能会有所不同。

写锁定保护修改可能如下所示:

func WriteTestMap(k, v int) {
    testMapLock.Lock()
    defer testMapLock.Unlock()
    testMap[k] = v
}

现在,如果你在for的块中释放读锁定,则并发goroutine可以自由获取写锁并对地图进行修改。在您的代码中:

testMapLock <- true
iteratorChannel <- k
<-testMapLock

k上发送iteratorChannel时,并发goroutine可能会修改地图。这不仅仅是一个不吉利的&#34;场景,在频道上发送一个值通常是一个阻止&#34;操作,如果通道的缓冲区已满,则必须准备好接收另一个goroutine,以便继续进行发送操作。在通道上发送值是一个很好的调度点,运行时甚至可以在同一个OS线程上运行其他goroutine,更不用说是否存在多个OS线程,其中一个可能已经在等待&#34;用于写锁定以执行地图修改。

总结最后一部分:你释放for区块内的读锁定就像是在向别人喊叫:&#34;来吧,如果你敢的话,现在就修改地图!&#34;因此,在您的代码中遇到mySeq != testMapSequence很可能。请参阅此runnable示例以演示它(它是您示例的变体):

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

最后的想法

语言规范明确允许您在同一个goroutine 中修改地图,同时覆盖它,这是前一个引用所涉及的内容(&#34;如果有地图条目迭代期间删除了尚未到达的内容....如果在迭代期间创建了地图条目...&#34; )。允许在同一goroutine中修改映射并且是安全的,但未定义迭代器逻辑如何处理它。

如果在另一个goroutine中修改了映射,如果使用正确的同步,The Go Memory Model保证带有for ... range的goroutine将观察所有修改,并且迭代器逻辑将看到它就像& #34;它自己的&#34; goroutine会对它进行修改 - 如前所述,这是允许的。

答案 1 :(得分:3)

forrange map循环的并发访问单位是地图。 Go maps in action

映射是一种动态数据结构,可以更改插入,更新和删除。 Inside the Map Implementation。例如,

  

未指定地图上的迭代顺序,并且无法保证   从一次迭代到下一次迭代都是一样的。如果映射条目   在迭代期间,尚未到达的部分被删除了   不会产生相应的迭代值。如果是地图条目   在迭代期间创建,该条目可以在期间生成   迭代或可以跳过。每个条目的选择可能不同   创建并从一次迭代到下一次迭代。如果地图是零,那么   迭代次数为0. For statements, The Go Programming Language Specification

使用带有交错插入,更新和删除的for range循环读取地图不太可能有用。

锁定地图:

package main

import (
    "sync"
)

var racer map[int]int

var race sync.RWMutex

func Reader() {
    race.RLock() // Lock map
    for k, v := range racer {
        _, _ = k, v
    }
    race.RUnlock()
}

func Write() {
    for i := 0; i < 1e6; i++ {
        race.Lock()
        racer[i/2] = i
        race.Unlock()
    }
}

func main() {
    racer = make(map[int]int)
    Write()
    go Write()
    Reader()
}

请勿在阅读后锁定 - fatal error: concurrent map iteration and map write

package main

import (
    "sync"
)

var racer map[int]int

var race sync.RWMutex

func Reader() {
    for k, v := range racer {
        race.RLock() // Lock after read
        _, _ = k, v
        race.RUnlock()
    }
}

func Write() {
    for i := 0; i < 1e6; i++ {
        race.Lock()
        racer[i/2] = i
        race.Unlock()
    }
}

func main() {
    racer = make(map[int]int)
    Write()
    go Write()
    Reader()
}

使用Go Data Race Detector。阅读Introducing the Go Race Detector