主语言转成Go了,记录一些Go的学习笔记与心得,可能有点凌乱。内容来自于《Go程序设计语言》,这本书强烈推荐。编程
(Go中并发编程是使用的Go独有的goroutine,不能彻底等同于线程,但这不是这篇的重点,下面不作区分了)缓存
在串行程序中,程序中各个步骤的执行顺序由程序逻辑决定。好比,在一系列语句中,第一句在第二句以前执行,以此类推。当一个程序中有多个goroutine时,每一个goroutine内部的各个步骤也是按顺序执行的,但咱们不能肯定一个goroutine中的事件x与另外一个goroutine中的事件y的前后顺序。若是咱们没法自信地说一个事件确定先于另一个事件,那么这两个事件就是并发的。(嗯,换了个角度理解并发,这个定义也确实有道理.架构
关于并发编程会产生的问题,想必诸位都很清楚。诸如不一样的线程操做相同的数据,形成的数据丢失,不一致,更新失效等等。在Go中关于并发产生的问题,重点能够讨论一下“竞态”----在多个goroutine按某些交错顺序执行时程序没法给出正确的结果。竞态对于程序是致命的,由于它们可能会潜伏在程序中,出现频率很低,极可能仅在高负载环境或者在使用特定的编译器,平台和架构时才出现。这些都使得竞态很难再现和分析。并发
数据竞态发生于两个goroutine并发读写同一个变量而且至少其中一个是写入时。从定义出发,咱们有几种方法能够规避数据竞态。学习
第一种方法--不要修改变量(有点幽默,但也有效。每一个线程都不会去写数据,天然也不会发生数据竞态的问题线程
第二种方法--避免竞态的方法是避免从多个goroutine访问同一个变量.即咱们只容许惟一的一个goroutine访问共享的资源,不管有多少个goroutine在作别的操做,当他们须要更改访问共享资源时都要使用同一个goroutine来实现,而共享的资源也被限制在了这个惟一的goroutine内,天然也就不会产生数据竞态的问题。这也是Go这门语言的思想之一 ---- 不要经过共享内存来通讯,要经过通讯来共享内存.Go中能够用chan来实现这种方式.(关于Chan能够看看笔者前面的博客哟设计
var deposits = make(chan int) //发送存款余额 var balances = make(chan int) //接收余额 func Deposit(amount int) {deposits <- amount} func Balance() int {return <- balances} func teller() { var balance int // balance被限制在 teller goroutine 中 for { select { case amount := <-deposits: balance += amount case balances <- balance: } } } func init() { go teller() }
这个简单的关于银行的例子,能够看出咱们把余额balance限制在了teller内部,不管是更新余额仍是读取当前余额,都只能经过teller来实现,所以避免了竞态的问题.协程
这种方式还能够拓展,即便一个变量没法在整个生命周期受限于当个goroutine,加以限制仍然能够是解决并发访问的好方法。好比一个常见的场景,能够经过借助通道来把共享变量的地址从上一步传到下一步,从而在流水线上的多个goroutine之间共享该变量。在流水线中的每一步,在把变量地址传给下一步后就再也不访问该变量了,这样全部对于这个变量的访问都是串行的。这中方式有时也被称为“串行受限”. 代码示例以下blog
type Cake struct {state string} func baker(cooked chan <- *Cake) { for { cake := new(Cake) cake.state = "cooked" cooked <- cake // baker再也不访问cake变量 } } func icer(iced chan<- *Cake, cooked <-chan *Cake) { for cake := range cooked { cake.state = "iced" iced <- cake // icer再也不访问cake变量 } }
第三种避免数据竞态的办法是容许多个goroutine访问同一个变量,但在同一时间内只有一个goroutine能够访问。这种方法称为互斥机制。通俗的说,这也就是咱们常在别的地方使用的“锁”。生命周期
Go中的互斥锁是由 sync.Mutex提供的。它提供了两个方法Lock用于上锁,Unlock用于解锁。一个goroutine在每次访问共享变量以前,它都必须先调用互斥量的Lock方法来获取一个互斥锁,若是其余的goroutine已经取走了互斥锁,那么操做会一直阻塞到其余goroutine调用Unlock以后。互斥变量保护共享变量。按照惯例,被互斥变量保护的变量声明应当紧接在互斥变量的声明以后。若是实际状况不是如此,请确认已加了注释来讲明此事.(深有同感,这确实是一个好的编程习惯)
加锁与解锁应当成对的出现,特别是当一个方法有不一样的分支,请确保每一个分支结束时都释放了锁。(这点对于Go来讲是特别的,一方面,Go语言的思想倡导尽快返回,一旦有错误就尽快返回,尽快的recover, 这就致使了一个方法中可能会有多个分支都返回。另外一方面,因为defer方法,使咱们没必要在每一个返回分支末尾都添上解锁或释放资源等操做,只要统一在defer中处理便可。)针对于互斥锁,结合咱们前面的银行的例子的那部分的代码,咱们来看一个有意思的问题。
//注意,这里不是原子操做 func withdraw(amount int) bool { Deposit(-amount) if Balance() < 0 { Deposit(amount) return false } return true }
逻辑很简单,咱们尝试提现。若是提现后余额小于0,则恢复余额,并返回false,不然返回true. 当咱们给Deposit与Balance的内部都加上锁,来保证互斥访问的时候,会有一个有意思的问题.首先要说明的是,这个方法是针对它自己的逻辑----可否提现成功,老是能够正确的返回的。但反作用时,在进行超额提现时,在Deposit与Balance之间,余额是会下降到0如下的。换成实际一点的状况就是,你和你媳妇的共享的银行卡里有10w,你尝试买一辆法拉利时,致使了你媳妇买一杯咖啡付款失败了,而且失败缘由是--余额不足。这种状况的根源是,Deposit与Balance两个方法内的锁是割裂开的,并非一个原子操做,也就是说,给了别的goroutine的可乘之机。虽然最终余额方面的数据老是对的,但过程当中也会发送诸如此类的错误。那若是咱们用这样的实现呢:
//注意,这里是错误的实现 func withdraw(amount int) bool { mu.Lock() defer mu.Unlock() Deposit(-amount) if Balance() < 0 { Deposit(amount) return false } return true }
即尝试给withdraw自己加锁。固然实际上,这是行不通的。因为Deposit内部也在加锁,这样的写法最终会致使死锁。一种改良方式是,分别实现包内可访问的deposit方法(在调用处外部提供锁,本身自己无锁),以及包外能够访问的Deposit(本身自己提供了互斥锁), 这样,在诸如提现这种须要同时使用更新余额/查余额的地方,咱们就可使用deposit来处理,并在提现方法自己提供锁来保证原子性。
固然,Go也支持读写锁 sync.RWMutex. 关于读写锁就很少bb了,但有一点要注意,只有在大部分goroutine都在获取读锁,而且锁竞争很激烈时,RWMutex才有优点,由于RWMutex须要更加复杂的内部记录工做,因此在竞争不激烈时它比普通的互斥锁要慢。
另外,书中提到因为现代计算机自己的多核机制以及Go中协程的实现,致使在一些无锁的状况下(且两个goroutine在不一样的CPU上执行,每一个CPU都有本身的缓存),可能致使goroutine拿不到最新的值。不过这种方式一来比较极端,二来能够经过简单且成熟的模式来避免。----在可能的状况下,把变量限制在单个goroutine内,对于其余的变量,采用互斥锁。 对于这部分感兴趣的同窗,能够去搜一下Go的内存同步,或者直接找《Go程序设计语言》内存同步这一节看一下。