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

问题描述 投票:9回答:2

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 - 用例完全不同,我在这里询问'range'关键字的精确锁定要求!)

for-loop dictionary go foreach concurrency
2个回答
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声明读取的吗?在什么情况下会发生这种情况?我们这里有一个正在执行someFunc()的goroutine。为了能够通过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()中的解锁应该(必须)作为延迟语句发生,就像在原始代码中一样,您可能会“早期”返回错误,在这种情况下您没有解锁,这意味着地图保持锁定状态! (这里由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可能会修改地图。这不仅仅是一种“不幸”的情况,在通道上发送值通常是“阻塞”操作,如果通道的缓冲区已满,则必须准备好接收另一个goroutine,以便继续执行发送操作。在通道上发送值是一个很好的调度点,运行时甚至可以在同一个OS线程上运行其他goroutine,更不用说是否存在多个OS线程,其中一个可能已经按顺序“等待”写入锁定进行地图修改。

总结最后一部分:你释放for区块内的读锁定就像是在向别人大喊:“来吧,如果你敢的话,现在修改地图吧!”因此,在您的代码遇到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中的地图,同时覆盖它,这是前一个引用所涉及的内容(“如果在迭代期间删除了尚未到达的地图条目....如果地图条目是在迭代期间创建......“)。允许在同一goroutine中修改映射并且是安全的,但未定义迭代器逻辑如何处理它。

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


3
投票

for range循环在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

© www.soinside.com 2019 - 2024. All rights reserved.