Go博客中的“ Go maps in action ”条目指出:
地图不能安全地并发使用:未定义同时读取和写入地图时会发生什么情况。如果您需要从并发执行的goroutine中读取和写入映射,则必须通过某种同步机制来调解访问。保护地图的一种常见方法是使用sync.RWMutex。
但是,一种常见的访问地图的方法是使用range
关键字遍历它们。出于并发访问的目的,不清楚在一个range
循环内执行是“读”还是该循环的“转换”阶段。例如,以下代码可能会或可能不会运行“在地图上没有并发读/写”规则,具体取决于操作的特定语义/实现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并发地图访问 -用例是完全不同的,我在这里询问有关“范围”关键字的精确锁定要求!)
您正在使用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 <- true
iteratorChannel <- k
<-testMapLock
在上发送k
时iteratorChannel
,并发goroutine可能会修改地图。这不仅是一种“不幸”的情况,在通道上发送值通常是“阻塞”操作,如果通道的缓冲区已满,则必须准备好接收另一个goroutine才能使发送操作继续进行。在通道上发送值是运行时甚至在同一OS线程上运行其他goroutine的良好调度点,更不用说是否有多个OS线程,其中一个OS线程可能已经在“等待”顺序进行写锁定进行地图修改。
总结一下最后一部分:释放for
块内的读取锁就像对别人大喊大叫:“来吧,如果您敢,请立即修改地图!” 因此,在您的代码中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
最后的想法
语言规范清楚地允许您 在同一goroutine 范围内修改地图,这就是前面的引用所涉及的内容(
“如果在迭代过程中删除了尚未到达的地图条目…。在迭代过程中创建…”
)。允许在同一goroutine中修改地图,这是安全的,但未定义迭代器逻辑如何处理地图。
如果在另一个goroutine中修改了映射,如果您使用适当的同步,则Go Memory
Model会保证带有的goroutine for ...range
将遵守所有修改,并且迭代器逻辑将看到它,就好像“自己的” goroutine将对其进行修改一样–如前所述,这是允许的。
问题内容: 如果要动态使用全局函数和变量,可以使用: 是否可以对本地范围内的变量执行相同的操作? 这段代码可以正常工作,但是目前使用eval,我正在尝试其他方法。 问题答案: 不,就像新月说的那样。在下面,您可以找到一个示例,该示例说明如何在不使用eval的情况下但使用内部私有对象进行实施。
问题内容: 在包中的某个动作内,是否可以在range动作之前访问管道值,或者是否可以将父/全局管道作为参数传递给Execute? 工作示例显示了我尝试执行的操作: play.golang.org 问题答案: 使用$变量(推荐) 从软件包文本/模板文档中: 开始执行时,将$设置为传递给Execute的数据参数,即dot的起始值。 正如@Sandy所指出的,因此可以使用来访问外部作用域中的Path 。
问题内容: 如果我有以下控制器: 读出的正确方法是什么?如果有必要定义中,也不会使它语义上不正确假设是描述相关的东西的属性,而不是? 更新: 对此进行进一步的思考,如果一个孩子有多个孩子,将会在检索上产生冲突。我的问题是,什么是访问的正确方法是从? 问题答案: AngularJS中的作用域使用原型继承,当在子作用域中查找属性时,解释器将从子对象开始查找原型链,并继续寻找父对象,直到找到该属性为止,
授权端点和令牌端点允许客户端使用“scope”请求参数指定访问请求的范围。反过来,授权服务器使用“scope”响应参数通知客户端颁发的访问令牌的范围。 范围参数的值表示为以空格分隔,大小写敏感的字符串。 由授权服务器定义该字符串。如果该值包含多个空格分隔的字符串,他们的顺序并不重要且每个字符串为请求的范围添加一个额外的访问区域。 scope = scope-token *( SP scope-to
问题内容: 在或中时,将更改的范围。您如何访问调用范围? 问题答案: 记录在文本/模板文档中: 开始执行时,将$设置为传递给Execute的数据参数,即dot的起始值。
问题内容: 我正在试用Go的新模块系统,无法访问本地软件包。以下项目位于我的gopath外的桌面上的文件夹中。 我的项目结构如下: 告诉我 问题答案: 我强烈建议您使用go工具链,它可以解决这些问题。带vscode-go插件的Visual Studio Code非常有用。 这里的问题是Go需要相对于您或import语句的相对路径。根据您所在的位置,导入路径也应包括该位置。在这种情况下,import