在 Go 语言中,如何正确的使用并发

从多个花絮中提取,但是如果我斗胆提出主要观点的总结,其内容就是:抢占式多任务和一般共享状态结合导致软件开发过程不可管理的复杂性, 开发人员可能更喜欢保持自己的一些理智以此避免这种不可管理的复杂性。抢占式调度对于哪些真正的并行任务是好的,但是当可变状态通过多并发线程共享时,明确的多任务合作更招人喜欢 。

尽管合作多任务,你的代码仍有可能是复杂的,它只是有机会保持可管理下一定的复杂性。当控制转移是明确一个代码阅读者至少有一些可见的迹象表明事情可能脱离正轨。没有明确标记每个新阶段是潜在的地雷:“如果这个操作不是原子操作,最后出现什么情况?”那么在每个命令之间的空间变成无尽的空间黑洞,可怕的Heisenbugs出现

在过去的一年多,尽管在Heka上的工作(一个高性能数据、日志和指标处理引擎)已大多数使用GO语言开发。Go的亮点之一就是语言本身有一些非常有用的并发原语。但是Go的并发性能怎么样,需要通过支持本地推理的鼓励代码镜头观察。

并非事实都是好的。所有的Goroutine访问相同的共享内存空间,状态默认可变,但是Go的调度程序不保证在上下文选择过程中的准确性。在单核设置中,Go的运行时间进入“隐式协同工作”一类, 在Glyph中经常提到的异步程序模型列表选择4。 当Goroutine能够在多核系统中并行运行,世事难料。

Go不可能保护你,但是并不意味着你不能采取措施保护自己。在写代码过程中通过使用一些Go提供的原语,可最小化相关的抢占式调度产生的异常行为。请看下面Glyph示例“账号转换”代码段中Go接口(忽略哪些不易于最终存储定点小数的浮点数)

func Transfer(amount float64, payer, payee *Account,
server SomeServerType) error {
if payer.Balance() < amount {
return errors.New("Insufficient funds")
}
log.Printf("%s has sufficient funds", payer)
payee.Deposit(amount)
log.Printf("%s received payment", payee)
payer.Withdraw(amount)
log.Printf("%s made payment", payer)
server.UpdateBalances(payer, payee) // Assume this is magic and always works.
return nil
}
这明显的是不安全的,如果从多个goroutine中调用的话,因为它们可能并发的从存款调度中得到相同的结果,然后一起请求更多的已取消调用的存款变量。最好是代码中危险部分不会被多goroutine执行。在此一种方式实现了该功能:

type transfer struct {
payer *Account
payee *Account
amount float64
}
var xferChan = make(chan *transfer)
var errChan = make(chan error)
func init() {
go transferLoop()
}
func transferLoop() {
for xfer := range xferChan {
if xfer.payer.Balance < xfer.amount {
errChan <- errors.New("Insufficient funds")
continue
}
log.Printf("%s has sufficient funds", xfer.payer)
xfer.payee.Deposit(xfer.amount)
log.Printf("%s received payment", xfer.payee)
xfer.payer.Withdraw(xfer.amount)
log.Printf("%s made payment", xfer.payer)
errChan <- nil
}
}
func Transfer(amount float64, payer, payee *Account,
server SomeServerType) error {
xfer := &transfer{
payer: payer,
payee: payee,
amount: amount,
}
xferChan <- xfer
err := <-errChan
if err == nil {
server.UpdateBalances(payer, payee) // Still magic.
}
return err
}
这里有更多代码,但是我们通过实现一个微不足道的事件循环消除并发问题。当代码首次执行时,它激活一个goroutine运行循环。转发请求为了此目的而传递入一个新创建的通道。结果经由一个错误通道返回到循环外部。因为通道不是缓冲的,它们加锁,并且通过Transfer函数无论多个并发的转发请求怎么进,它们都将通过单一的运行事件循环被持续的服务。

上面的代码看起来有点别扭,也许吧. 对于这样一个简单的场景一个互斥锁(mutex)也许会是一个更好的选择,但是我正要尝试去证明的是可以向一个go例程应用隔离状态操作. 即使稍稍有点尴尬,但是对于大多数需求而言它的表现已经足够好了,并且它工作起来,甚至使用了最简单的账号结构实现:

type Account struct {
balance float64
}
func (a *Account) Balance() float64 {
return a.balance
}
func (a *Account) Deposit(amount float64) {
log.Printf("depositing: %f", amount)
a.balance += amount
}
func (a *Account) Withdraw(amount float64) {
log.Printf("withdrawing: %f", amount)
a.balance -= amount
}
不过如此笨拙的账户实现看起来会有点天真. 通过不让任何大于当前平衡的撤回操作执行,从而让账户结构自身提供一些保护也许更起作用。那如果我们把撤回函数变成下面这个样子会怎么样呢?

func (a *Account) Withdraw(amount float64) {
if amount > a.balance {
log.Println("Insufficient funds")
return
}
log.Printf("withdrawing: %f", amount)
a.balance -= amount
}
不幸的是,这个代码患有和我们原来的 Transfer 实现相同的问题。并发执行或不幸的上下文切换意味着我们可能以负平衡结束。幸运的是,内部的事件循环理念应用在这里同样很好,甚至更好,因为事件循环 goroutine 可以与每个个人账户结构实例很好的耦合。这里有一个例子说明这一点:

type Account struct {
balance float64
deltaChan chan float64
balanceChan chan float64
errChan chan error
}
func NewAccount(balance float64) (a *Account) {
a = &Account{
balance: balance,
deltaChan: make(chan float64),
balanceChan: make(chan float64),
errChan: make(chan error),
}
go a.run()
return
}
func (a *Account) Balance() float64 {
return <-a.balanceChan
}
func (a *Account) Deposit(amount float64) error {
a.deltaChan <- amount
return <-a.errChan
}
func (a *Account) Withdraw(amount float64) error {
a.deltaChan <- -amount
return <-a.errChan
}
func (a *Account) applyDelta(amount float64) error {
newBalance := a.balance + amount
if newBalance < 0 {
return errors.New("Insufficient funds")
}
a.balance = newBalance
return nil
}
func (a *Account) run() {
var delta float64
for {
select {
case delta = <-a.deltaChan:
a.errChan <- a.applyDelta(delta)
case a.balanceChan <- a.balance:
// Do nothing, we've accomplished our goal w/ the channel put.
}
}
}
这个API略有不同,Deposit 和 Withdraw 方法现在都返回了错误。它们并非直接处理它们的请求,而是把账户余额的调整量放入 deltaChan,在 run 方法运行时的事件循环中访问 deltaChan。同样的,Balance 方法通过阻塞不断地在事件循环中请求数据,直到它通过 balanceChan 接收到一个值。

须注意的要点是上述的代码,所有对结构内部数据值得直接访问和修改都是有事件循环触发的 within 代码来完成的。如果公共 API 调用表现良好并且只使用给出的渠道同数据进行交互的话, 那么不管对公共方法进行多少并发的调用,我们都知道在任意给定的时间只会有它们之中的一个方法得到处理。我们的时间循环代码推理起来更加容易了很多。

该模式的核心是 Heke 的设计. 当Heka启动时,它会读取配置文件并且在它自己的go例程中启动每一个插件. 随着时钟信号、关闭通知和其它控制信号,数据经由通道被送入插件中. 这样就鼓励了插件作者使用一种想上述事例那样的 事件循环类型的架构 来实现插件的功能.

再次,GO不会保护你自己. 写一个同其内部数据管理和主题有争议的条件保持松耦合的Heka插件(或者任何架构)是完全可能的。但是有一些需要注意的小地方,还有Go的争议探测器的自由应用程序,你可以编写的代码其行为可以预测,甚至在抢占式调度的门面代码中。

英文原文:Sane Concurrency with Go

译文链接:http://www.oschina.net/translate/sane-concurrency-with-go

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容