内容简介:过去 Web 开发的工作比较少涉及到并发的问题,每个用户请求在独立的线程里面进行,偶尔涉及到异步任务但是线程间数据同步模型非常简单,因此并未深入探究过并发这一块。最近在写游戏相关的服务端代码时发现数据的并发同步场景非常多,因此花了一点时间来探索。文本通过一个例子来简单引出 Golang 中的数据并发同步问题,并通过简单加锁的方式来避免数据竞争问题。下面的代码模拟了为一个用户(
写在前面
过去 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
,并通过添加两个方法 GetMoney
和 AddMoney
来达到读取和修改 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 中数据的并发同步问题,并通过加锁的方式避免了 数据竞争 问题。加锁在各个语言中都是一种常见的方式,理解起来是比较容易的,因此本文并没有对加锁机制进行进一步的阐述。
不过,数据的并发同步是一个涉及很广泛的问题,接下来需要继续总结。
参考
- 谈谈go语言编程的并发安全 比较透彻地介绍了 Golang 中并发安全的问题(加锁场景)
- benign-data-races-what-could-possibly-go-wrong Intel 工程师讲数据竞争问题
- sync - The Go Programming Language Golang 官方提供的 sync 包的文档
- The Go Memory Model - The Go Programming Language Golang 中内存模型的建议(Advice)
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Java 并发编程:多线程并发访问,同步控制
- Java 并发编程:多线程并发访问,同步控制
- 深入Linux并发同步
- Golang 并发编程与同步原语
- 多线程六 同步容器&并发容器
- 浅谈 Golang 中数据的并发同步问题(三)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java 8实战
厄马(Raoul-Gabriel Urma)、弗斯科(Mario Fusco)、米克罗夫特(Alan Mycroft) / 陆明刚、劳佳 / 人民邮电出版社 / 2016-4-1 / CNY 79.00
本书全面介绍了Java 8 这个里程碑版本的新特性,包括Lambdas、流和函数式编程。有了函数式的编程特性,可以让代码更简洁,同时也能自动化地利用多核硬件。全书分四个部分:基础知识、函数式数据处理、高效Java 8 编程和超越Java 8,清晰明了地向读者展现了一幅Java 与时俱进的现代化画卷。一起来看看 《Java 8实战》 这本书的介绍吧!