浅谈 Golang 中数据的并发同步问题(三)

栏目: Go · 发布时间: 5年前

内容简介:过去 Web 开发的工作比较少涉及到并发的问题,每个用户请求在独立的线程里面进行,偶尔涉及到异步任务但是线程间数据同步模型非常简单,因此并未深入探究过并发这一块。最近在写游戏相关的服务端代码时发现数据的并发同步场景非常多,因此花了一点时间来探索。这是一个系列文章,本文为第三篇。本文简单介绍 Golang 中 map 类型的安全使用。在业务逻辑中保存

写在前面

过去 Web 开发的工作比较少涉及到并发的问题,每个用户请求在独立的线程里面进行,偶尔涉及到异步任务但是线程间数据同步模型非常简单,因此并未深入探究过并发这一块。最近在写游戏相关的服务端代码时发现数据的并发同步场景非常多,因此花了一点时间来探索。这是一个系列文章,本文为第三篇。

本文简单介绍 Golang 中 map 类型的安全使用。

Golang 中 map 的使用

在业务逻辑中保存 key-value 是一个非常普遍的需求,因此 Map 的使用场景非常多。

不允许并发读写的 map

在 Golang 源码实现中对 map 的要求比较高(见《 Go maps in action 》): Maps are not safe for concurrent use: it's not defined what happens when you read and write to them simultaneously (当并发使用时 Maps 是不安全的,当并发地读写 map 的时候无法预知会发生啥 )。

如果不加保护地在不同的线程中读写 map 类型的数据,代码会直接崩溃并异常退出。比如下面的代码:

package main

func main() {
	m := make(map[int]int)
	go func() {
		for {
			_ = m[1]
		}
	}()
	go func() {
		for {
			m[2] = 1
		}
	}()
	select {}
}

运行上面的代码可以得到下面类似的结果:

go run map/main.go 
# fatal error: concurrent map read and map write
# ....(省略异常堆栈)

从输出结果来看,Golang 运行时明确禁止 map 的并发读写,且在检测到这种情况后直接异常退出。这不同于其他数据类型,比如 intstring 等,对比下面的代码(说明:下面的代码存在隐形的并发问题,具体参考《 浅谈 Golang 中数据的并发同步问题(二) 》):

// 运行下面的代码并不会异常退出,不同于上面 map 类型的 m 的使用
// go run main.go 
package main

func main() {
	var m int
	go func() {
		for {
			_ = m
		}
	}()
	go func() {
		for {
			m = 1
		}
	}()
	select {}
}

再次 需要说明 ,虽然上面的代码在不同的线程中访问 int 类型的数据并未直接异常退出,但是这种不加任何安全措施的并发读写是存在安全风险的,具体参考《 浅谈 Golang 中数据的并发同步问题(二) 》。

安全使用 map——显而易见地加锁

既然 Golang 在运行时不允许对 map 的并发读写,当需要在多个线程中读写 map 时,显而易见的方式是 加锁 (如《 浅谈 Golang 中数据的并发同步问题(一) 》所描述的)。

下面的代码把 map 类型的 m 封装在一个匿名的 struct 中,同时整个匿名的 struct 继承了 sync.RWMutex 结构,因此拥有了 加读写锁 的功能,从而安全地实现了多个线程对 map 的 “并发读写”:

package main

import (
	"sync"
)

func main() {
	var counter = struct {
		sync.RWMutex
		m map[string]int
	}{m: make(map[string]int)}

	go func() {
		for {
			counter.RLock()
			_ = counter.m["some_key"]
			counter.RUnlock()
		}
	}()
	go func() {
		for {
			counter.Lock()
			counter.m["some_key"]++
			counter.Unlock()
		}
	}()
	select {}
}

为什么 map 并发读写时会在运行时异常退出

最后提一下这个问题: 为什么 int、string、slice 等变量在多个线程读写时运行正常,而 map 在多个线程并发读写时会运行时异常退出? 其实这个涉及到 map 的具体实现(我知道这是一句废话 +_+)。

简单来讲,可以从 Go 源码中 map 运行时相关的部分 窥见一些依据: map 的增改删查可以分别对应到 func mapassign()func mapaccess1()func mapdelete() 这几个函数,每个函数都有非常长的执行逻辑;如果多个线程并发读写同一个 map ,大概率会出现 ① mapassign 函数(增加某个 key 的值)执行到一半的时候 mapaccess1 读取到一个相应的零值,② mapaccess1 函数(读取某个 key 的值)执行到一半的时候 mapdelete 已经删除了对应的 key ,等等。

同时考虑到增删数据时底层数据的改变(比如扩容重分配,这一块还没深入研究,可以自行查看源码=。=),因此保持 map 的单纯变得很重要;为避免出现难以 debug 的异常, 运行时环境显式地并发异常退出也就可以理解了。

小结

Golang 的运行时会 在 map 的增改删查过程中检测是否有并发读写的情况,当发现并发读写时直接异常退出 。相对于其他数据类型(比如 int、string、slice 等),map 的并发使用是比较严苛的(安全&性能的折中);可以认为 map 的这种严苛很大程度上降低了诡异 bug 的产生,增加代码的鲁棒性。

最后,当提到 map 的并发使用时,很多时候会提到 sync.Map 的使用,不过由于它大量使用了 interface{} 类型,使用起来并不是那么方便;目前为止, 我更喜欢加读写锁的方式 来使用 map 而不是使用线程安全的 sync.Map :laughing:

参考


以上所述就是小编给大家介绍的《浅谈 Golang 中数据的并发同步问题(三)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

从界面到网络空间

从界面到网络空间

(美)海姆 / 金吾伦/刘钢 / 上海科技教育出版社 / 2000-7 / 16.40元

计算机急剧改变了20世纪的生活。今天,我们凭借遍及全球的计算机网络加速了过去以广播、报纸和电视形式进行的交流。思想风驰电掣般在全球翻飞。仅在角落中潜伏着已完善的虚拟实在。在虚拟实在吕,我们能将自己沉浸于感官模拟,不仅对现实世界,也对假想世界。当我们开始在真实世界与虚拟世界之间转换时,迈克尔·海姆问,我们对实在的感觉如何改变?在〈从界面到网络空间〉中,海姆探讨了这一问题,以及信息时代其他哲学问题。他......一起来看看 《从界面到网络空间》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具