首页 > 其他分享 >从 fatal 错误到 sync.Map:Go中 Map 的并发策略

从 fatal 错误到 sync.Map:Go中 Map 的并发策略

时间:2024-01-23 18:56:00浏览次数:40  
标签:Map map sync 并发 Go fatal

从 fatal 错误到 sync.Map:Go中 Map 的并发策略

原创 波罗学 码途漫漫 2024-01-21 21:00 发表于上海 听全文

为什么 Go 语言在多个 goroutine 同时访问和修改同一个 map 时,会报出 fatal 错误而不是 panic?我们该如何应对 map 的数据竞争问题呢?

码途漫漫 踏实地写好每一篇技术文。 27篇原创内容 公众号

这篇文章将带你一步步了解背后的原理,并引出解决 map 并发问题的方案。

Map 数据竞争

首先,什么是 Map 数据竞争。

当两个或多个 goroutine 在没有适当同步机制的情况下,同时访问同一块数据,且至少有一个 goroutine 在修改这块数据,就会发生数据竞争。这种情况可能导致程序的行为异常,甚至崩溃。

图片

而 map 是 Go 中的一种常用的数据结构,提供了快速的 Key/Value 存储能力。但 Go 默认的 map 并不提供并发安全。这意味着,如果我们没有采取措施来控制 map 同步访问,如果多个 goroutine 同时对一个map进行读写操作,就可能会引发数据竞争。

Map 数据竞争产生 fatal error

在 Go 语言中,处理错误的方式通常是通过返回 Error 或者 panic。然而一旦程序检测到 map 的数据竞争,就会抛出 fatal 错误。而 fatal error 即意味会立刻崩溃。毫无疑问,Go 选择了更严格的处理方式。

通过一个简单的例子演示 fatal 错误是如何被触发的:

package main

import (
    "sync"
)

func main() {
    m := make(map[int]int)
    wg := sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                m[j] = j
            }
        }()
    }
    wg.Wait()
}

这个例子中,我创建了一个map 类型变量 m,然后启动了10个 goroutine,每个 goroutine 都尝试向map中写入 1000 个键值对。由于 map 在 Go 中不是并发安全的,这将导致数据竞争。

这个代码可能会触发如下的 fatal 错误,输出如下所示:

fatal error: concurrent map writes

goroutine 6 [running]:

... 省略

exit status 2

为什么 fatal 错误, 而非 panic?

fatal 错误让我们没有在程序运行时进行补救。我猜测这背后的原因主要是以下两点:

图片

立即暴露问题

这种处理方式确保了一旦发生数据竞争,程序将立即停止运行,迫使我们直面问题,不能逃避。

这不仅有利于我们快速发现解决并发 bug,也促使我们编写代码时,应更注重并发安全,避免发生这类问题。

虽然,这种方式可能会导致程序在运行时突然停止,但长远来看,它有助于提高程序的健壮性和可靠性。

防止数据腐败

数据竞争的后果可能非常严重,尤其是在复杂的并发系统中。

当多个goroutine不协调地访问和修改同一个map时,可能会导致map的内部状态变得不一致,甚至损坏。

这种状态的不确定性不仅会导致程序行为异常,还可能导致难以追踪的bug。

深入源码:map 并发检测

当 Go 检测到 map 的并发写入,会通过 throw 函数抛出 fatal 错误。这一过程发生在 mapassign 函数中。

以下是简化后的 mapassign 函数的伪代码,:

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    // 检查是否有其他goroutine正在写入map
    if h.flags&hashWriting != 0 {
        throw("concurrent map writes")
    }
    
    // ...其他map赋值逻辑...
    
    // 设置标志位,表示有goroutine正在写入map
    h.flags |= hashWriting
    
    // ...执行map的赋值逻辑...
    
    // 写入完成,清除写入标志位
    h.flags &^= hashWriting
    
    return val
}

通过这段代码,就能理解 fatal 错误是如何被触发的。对,重点就是 h.flags&hashWriting 这段条件判断。

如何避免数据竞争

在 Go 中,最常用的并发控制机制是使用 channel 或 sync 包中的工具。另外,Go 还提供了一个并发安全的 map 类型 - sync.Map。

图片

sync.Mutex

我将通过以下这段代码演示如何使用 sync.Mutex 避免数据竞争:

package main

import (
    "sync"
)

func main() {
    m := make(map[int]int)
    var mu sync.Mutex
    wg := sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                mu.Lock()
                m[j] = j
                mu.Unlock()
            }
        }()
    }
    wg.Wait()
}

在这个例子中,我们使用了 sync.Mutex 确保每次只有一个 goroutine 可以写入 map,从而避免数据竞争,保证程序的稳定性和正确性。

sync.Map

虽然 Go 的标准 map 非并发安全,但 Go 在 1.9 版本中引入了一个并发安全的 map 类型 - sync.Map,专门设计来处理并发场景下的 Key/Value 存储。

sync.Map 有一些特别的特性,它不需要显式的锁操作来保证并发安全,为它内部已经处理好了同步机制,这可简化我们的并发编程。

以下示例使用 sync.Map 改写了之前通过 sync.Mutex 实现的代码。

package main

import (
    "sync"
    "fmt"
)

func main() {
    var sm sync.Map
    wg := sync.WaitGroup{}

    // 写入数据
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            sm.Store(n, n*n)
        }(i)
    }

    // 读取数据
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            if value, ok := sm.Load(n); ok {
                fmt.Printf("Key: %v, Value: %v\n", n, value)
            }
        }(i)
    }

    wg.Wait()
}

这个例子中,sync.Map 的 Store 方法用于存储 Key/Value,Load 方法用于读取数据。我们通过这种方式就可以在多个 goroutine 中安全地使用map,而不必担心数据竞争。

另外,sync.Map 相对于传统的 sync.Mutext + map 的组合,除简化了并发编程,它还针对并发场景做了一些优化,如无锁读、细粒度锁机制(非锁整个 Map)等等。更多细节,可自行研究。

总结

在本文中,我们探讨了 Go 语言处理 map 并发操作数据竞争情况下的处理方式。这种设计突显了 Go 对并发安全的重视。另外,通过 Go 提供的 sync.Mutex 和sync.Map 等工具,可有效避免数据竞争,确保我们构建出稳定和高效的并发应用。

我想说,对这些机制的理解对于我们编写出健壮的Go程序是至关重要的。

博文地址:从 fatal 错误到 sync.Map:Go中 Map 的并发策略[1]

引用链接

[1] 从 fatal 错误到 sync.Map:Go中 Map 的并发策略: https://www.poloxue.com/posts/2024-01-21-fatal-error-in-concurrent-accessing-map/

 

波罗学

赞赏二维码喜欢作者

golang 语言技巧3 golang 语言技巧 · 目录 上一篇GO 中高效 int 转换 string 的方法与源码剖析下一篇Go 语言中高效切片拼接和 GO 1.22 提供的新方法 阅读原文 阅读 58 码途漫漫 ​ 喜欢此内容的人还喜欢   GO 中高效 int 转换 string 的方法与源码剖析     我关注的号 码途漫漫 不看的原因   如果你还对Go并发编程感到迷茫,也许是好事情     鸟窝聊技术 不看的原因   【GoLang入门教程】Go语言工程结构详述     架构殿堂 不看的原因   2条留言 写留言
  •   神经蛙   北京2天前 回复   波大更新啦     码途漫漫   (作者)2天前 回复   还以为没人看了 静悄悄发发  
已无更多数据              

人划线

标签:Map,map,sync,并发,Go,fatal
From: https://www.cnblogs.com/cheyunhua/p/17983155

相关文章

  • arcengine GP调用PolygonToLine 报错 -2147467259
    这个原因是传参数问题;GP调用面转线工具时,不能利用该方式传入参数IGpValueTableObjectgpValueTableObject=newGpValueTableObject();//对一个及以上要素类进行相交运算gpValueTableObject.SetColumns(2);objecto1=pFeatureClass2;//输入IFeatureC......
  • GORM高级查询
    GORM高级查询准备数据typeStudentstruct{IDuint`gorm:"size:3"`Namestring`gorm:"size:8"`Ageint`gorm:"size:3"`GenderboolEmail*string`gorm:"size:32"`}func(stuStudent)TableName()s......
  • 斜45度瓦片地图(Staggered Tiled Map)里的简单数学
    转载至:ShuaiYUAN斜45度瓦片地图(StaggeredTiledMap)里的简单数学前段时间在做游戏的地图编辑功能,我们是在一个斜45度视角的场景上,对地图上的建筑或装饰物进行添加、移动、移除等基本操作,而且位置的改变是以网格作为最小操作单位的。本渣是用StaggeredTiledMap实现的,与垂直视......
  • go-carbon v2.3.6 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
    carbon是一个轻量级、语义化、对开发者友好的golang时间处理库,支持链式调用。目前已被awesome-go收录,如果您觉得不错,请给个star吧github.com/golang-module/carbongitee.com/golang-module/carbon安装使用Golang版本大于等于1.16//使用github库goget-ugithu......
  • PostMappering中consumes与produces属性的作用
     哈喽大家好今天跟大家简单聊一聊PostMappering中consumers与produces两个属性的作用在对接接口中,对方API要求,请求头HTTPHeader中设置Content-Type为application/x-www-form-urlencoded,响应头HTTPHeader中Content-Type为application/json。也就是说一个接口中,接收......
  • go教程7
    Mutexvs信道通过使用Mutex和信道,我们已经解决了竞态条件的问题。那么我们该选择使用哪一个?答案取决于你想要解决的问题。如果你想要解决的问题更适用于Mutex,那么就用Mutex。如果需要使用Mutex,无须犹豫。而如果该问题更适用于信道,那就使用信道。:)由于信道是Go语言很酷......
  • go教程6
    关闭信道和使用forrange遍历信道数据发送方可以关闭信道,通知接收方这个信道不再有数据发送过来。当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。v,ok:=<-ch上面的语句里,如果成功接收信道所发送的数据,那么 ok 等于true。而如果 ok 等于fals......
  • go教程8
    1.    合取代继承      Go不支持继承,但它支持组合(Composition)。组合一般定义为“合并在一起”。汽车就是一个关于组合的例子:一辆汽车由车轮、引擎和其他各种部件组合在一起。通过嵌套结构体进行组合在Go中,通过在结构体内嵌套结构体,可以实现组合。组合的典型例......
  • go教程10
    1.    自定义错误在上一教程里,我们学习了Go中的错误是如何表示的,并学习了如何处理标准库里的错误。我们还学习了从标准库的错误中提取更多的信息。在本教程中,我们会学习如何创建我们自己的自定义错误,并在我们创建的函数和包中使用它。我们会使用与标准库中相同的技术,来提......
  • go教程9
    什么是defer?defer 语句的用途是:含有 defer 语句的函数,会在该函数将要返回之前,调用另一个函数。这个定义可能看起来很复杂,我们通过一个示例就很容易明白了。示例packagemain import(     "fmt") funcfinished(){     fmt.Println("Finishedfindi......