围棋博客中的"Go maps in action"词条写道:

map 对于并发使用是不安全的:没有定义当您同时读取和写入 map 时会发生什么.如果需要从并发执行的goroutines读取和写入映射,则访问必须通过某种同步机制进行中介.保护 map 的一种常见方法是使用sync.RWMutex.

但是,访问 map 的一种常见方式是使用关键字range迭代 map .不清楚出于并发访问的目的,range循环内的执行是"读取",还是仅仅是该循环的"周转"阶段.例如,以下代码可能与" map 上无并发读写"规则冲突,也可能不冲突,具体取决于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
 }

这里的 idea 是,当第二个函数等待消费者获取下一个值时,range"迭代器"是打开的,写入程序当时没有被阻止.然而,一个迭代器中的两个读操作在一个写操作的两边都不是这种情况——这是一个"快速失败"迭代器,borrow Java术语.

然而,在语言规范或其他文件中是否有任何地方表明这是一件合法的事情?我可以看到这两种情况,上面引用的文件并不清楚什么是"阅读".关于for/range语句的并发性方面,文档似乎完全没有涉及.

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

推荐答案

您正在使用带有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()kv变量保存迭代的值之前,它们是 map 上的already read out(否则它们无法保存值).问题:你认为 map 是由for ... range语句between t1t2读取的吗?在什么情况下会发生这种情况?我们这里有一个single goroutine,执行someFunc().要想用for语句来绘制 map ,要么需要another个goroutine,要么需要suspendsomeFunc().显然这两种情况都没有发生.(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会修改它一样——这是允许的,如前所述.

Go相关问答推荐

如何在GoFr中为生产和本地环境设置不同的配置?

我怎样才能改进这个嵌套逻辑以使其正常工作并提高性能

如何找到一个空闲的TPM句柄来保存新的密钥对对象?

Kperf 构建失败

使用 httptest 对 http 请求进行单元测试重试

Go struct 匿名字段是公开的还是私有的?

替换字符串中的最后一个字符

Go 中的 Azure JWT 验证不起作用

Go 切片容量增长率

整理时转换值

Wire google Inject with multi return from provider 函数

通过环境变量配置 OTLP 导出器

无法使用 Golang 扫描文件路径

Golang - 客户 Unmarshaler/Marshaler 在指针上具有 nil/null 值

为什么import和ImportSpec之间可以出现多行注释,而PackageName和ImportPath之间不能出现?

在 connect-go 拦截器中修改响应体

使用 Go 读取 TOML 文件时结果为空

具有多个嵌入式 struct 的 Go MarshalJSON 行为

go mod tidy 错误消息:但是 go 1.16 会 Select

从 map 返回空数组而不是空字符串数组