55

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

 4 years ago
source link: https://jingwei.link/2019/05/11/golang-concurrency-one.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

写在前面

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

文本通过一个例子来简单引出 Golang 中的数据并发同步问题,并通过简单加锁的方式来避免数据竞争问题。

从一个例子看线程安全与数据竞争问题

一个非常原始的数据竞争问题

下面的代码模拟了为一个用户( 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
	}()
	fmt.Printf("Money: %d\n", p.Money)
}

运行上面的代码:

# 只是运行上面的代码
go run main.go
# Money: 100

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

通过运行代码并查看输出可以确认:① 代码可以正常运行(并没有因为多个线程读写同一个变量而崩溃),② 由于数据没有同步最后输出的数据 Money: 100 与预期的数据 1100 存在误差,③ 通过 go run -race main.go 添加 -race 标识可以发现 数据竞争 问题。

通过加锁的方式优化有数据竞争的代码

下面的代码里给 Person 添加了一个读写锁 mutext sync.RWMutex ,并通过添加两个方法 GetMoneyAddMoney 来达到读取和修改 Money 的数值的目的。

package main

import (
	"fmt"
	"sync"
)

type Person struct {
	Money  int
	mutext sync.RWMutex
}

// GetMoney 获取用户金钱
func (p *Person) GetMoney() int {
	p.mutext.RLock()
	defer p.mutext.RUnlock()
	money := p.Money
	return money
}

// AddMoney 设置用户金钱
func (p *Person) AddMoney(diff int) {
	p.mutext.Lock()
	defer p.mutext.Unlock()
	p.Money += diff
}

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

运行上面的代码:

# 添加 -race 后检测竞争状态,此时已经看不到 race 的告警
go run -race main.go
# Money: 100

通过运行代码并查看输出可以确认:① 代码可以正常运行(并没有因为多个线程读写同一个变量而崩溃),② 由于数据没有同步最后输出的数据 Money: 100 与预期的数据 1100 存在误差 ③ 已经没有了 数据竞争 问题。

示例代码的进一步阐释

上面的代码逻辑, 加锁前加锁后 最大的区别是:加锁前 存在数据竞争问题 ,加锁后 不存在数据竞争问题 。而无论是否加锁,代码均可以正常运行,且最终同步的数据与预期的数据均存在偏差。

之所以强调代码可以正常运行,是因为代码一定概率是会崩溃的,只是一般类型( map 类型除外)不那么容易出现崩溃的情况(任何类型变量的使用都可能会出现这个问题,详见《 谈谈go语言编程的并发安全 》 和 《 benign-data-races-what-could-possibly-go-wrong 》的讨论)。

由于运行时序的存在,读取得到的数据与预期的数据存在偏差可以这样解释:虽然期望里给用户加了 1000 金钱,但是如果读取是在 加 1000 金钱 之前发生的,也确实是感知不到 加 1000 金钱 这个事件的。

小结

本文简单介绍了 Golang 中数据的并发同步问题,并通过加锁的方式避免了 数据竞争 问题。加锁在各个语言中都是一种常见的方式,理解起来是比较容易的,因此本文并没有对加锁机制进行进一步的阐述。

不过,数据的并发同步是一个涉及很广泛的问题,接下来需要继续总结。

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK