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

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

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

写在前面

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

本文通过一个例子来引出 Golang 中的数据并发同步问题,并通过使用 atomic 包的方式来避免数据竞争问题。

原子性及 Go 中 atomic 包的使用

再提非常原始的数据竞争问题

下面的代码模拟了为一个用户( Person )发放金币( Money )的代码,其中金币发放与读取分别再不同的线程里完成(读取在主线程,发放在一个子线程)。

// cat main.go
package main

import (
	"fmt"
)

type Person struct {
	Money int
}

func main() {
	p := Person{Money: 100}
	go func() {
		p.Money += 1000
	}()
	p.Money += 1000
	fmt.Printf("Money: %d\n", p.Money)
}

go run 时使用 -race 标志运行上面的代码可以发现数据竞争提醒:

# 添加 -race 后检测竞争状态,可以看到 race 的提醒
go run -race main.go
# Money: 1100
# ==================
# WARNING: DATA RACE
# Write at 0x00c00001c0a0 by goroutine 6:
# Previous read at 0x00c00001c0a0 by main goroutine

根据《 谈谈go语言编程的并发安全 》 和 《 benign-data-races-what-could-possibly-go-wrong 》的讨论,上面的代码无法保证 fmt.Printf("Money: %d\n", p.Money) 这一句会输出什么,同时存在严重的问题(高并发下可能出现严重问题导致代码崩溃),因此有必要通过一定的措施避免这种不确定性。

原子性概念及其实现原理

原子操作是指不会被线程调度机制打断的操作;原子操作一旦开始,就一直运行到结束,中间不会有任何 context switch(上下文切换,从当前线程切换到另一个线程)。(from 百度百科)

对于编程语言中变量的修改,可以认为原子操作需满足下面的条件:① 变量的读写过程不可中断(不可以读到一半做其他事情,也不可写到一半做其他事情),② 变量的读写过程不可同时进行(不可以写变量的同时读取变量,防止读取到仅更新了一半的变量值;也不可以写变量的同时另一个线程也在写这个变量,防止其中一个更新无效)。

根据 Golang 官方 内存模型 文档中的描述 Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access. 以及 atomic 包的存在,可以认为 Golang 对同一块内存的并发读写访问 并不是原子性 的。简而言之,Golang 中对一个变量进行更新时,另一个线程可以并发地读这个变量的值,而读取到的值是内存不确定的(即不一定读到什么东西)。

使用 atomic 包来避免数据竞争

上篇文章《 浅谈 Golang 中数据的并发同步问题(一) 》采用 添加读写锁 的方式避免了数据竞争。对于 用户 -> 金币 这种模型(金币是一个数值而非其他的复杂类型),其实也可以采用 atomic 包来避免数据竞争问题。

下面的代码引入了 atomic 的使用,由于其仅支持 int32int64 这种显式定义字节长度的类型,因此代码中把 Money 修改为了 int64 类型。

package main

import (
	"fmt"
	"sync/atomic"
)

type Person struct {
	Money int64
}

func main() {
	p := Person{Money: 100}
	go func() {
		atomic.AddInt64(&p.Money, 1000)
	}()
	atomic.AddInt64(&p.Money, 1000)
	fmt.Printf("Money: %d\n", atomic.LoadInt64(&p.Money))
}

go run -race main.go 运行上面的代码可以发现数据竞争的提醒消失了:

# 添加 -race 后检测竞争状态,可以看到 race 的提醒
go run -race main.go
# Money: 1100

小结

atomic 包主要局限在 数值类型 的赋值、修改、读取、交换等操作,不可以处理像 structmap 这种高级类型,因此 atomic 的使用具有比较大的局限性。

就像文档中描述的: These functions (of atomics) require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package. Share memory by communicating; don't communicate by sharing memory.

除非知道自己在做什么,否则更推荐使用 channel (通道) 和 sync (锁)的方式处理数据同步问题。

参考


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

查看所有标签

猜你喜欢:

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

Beginning ASP.NET 4 in C# and Vb

Beginning ASP.NET 4 in C# and Vb

Imar Spaanjaars / Wrox / 2010-3-19 / GBP 29.99

This book is for anyone who wants to learn how to build rich and interactive web sites that run on the Microsoft platform. With the knowledge you gain from this book, you create a great foundation to ......一起来看看 《Beginning ASP.NET 4 in C# and Vb》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试