Go语言并发之道--笔记1

本文深入探讨Go语言中的并发原理和技术,包括竞争条件、原子性、内存访问同步等问题,并讲解如何利用Go的内置工具如goroutine和channel来实现高效并发。
1、竞争跳条件
当两个或对各操作必须按正确的顺序执行,当程序未保证这个顺序,就会发生竞争条件
示例:
var data int
go func() {    //使用关键字go并发的运行一个函数,即goroutine
   data++
}()
if data==0 {
   fmt.Printf("the value is %v.\n",data)
}
data数据的访问循序不确定,即data++ 和 data==0,会产生多种输出结果

使用休眠来调整数据的竞争性问题,从表面上看,解决了竞争性问题,但实际及逻辑上,是没有解决该问题的
示例:
var data int
go func() { data++ } ()
time.Sleep(1*time.Second)  //方式不优雅
if data==0 {
   fmt.Printf("the value is %v.\n",data)
}


2、原子性
操作的原子性可以根据当前定义的范围而改变
考虑原子性的原则:
首先定义上下文或范围,然后再考虑操作是否是原子性。
优化并发程序的一种方式


3、内存访问同步
示例:
var data int
go func() { data++ } ()
id data==0 {
   fmt.Println("the value is 0.")
} else {
   fmt.printf("the value is %v.\n",data)
}
示例中的三个临界区:
goroutine正在增加数据变量。
if语句检查数据的值是否为0。
fmt.Printf语句检索并输出数据的值。

演示会内存同步示例(非go惯用方法):
虽然解决了数据竞争,但是操作顺序任然不确定:
var memoryAccess sync.Mutex    //添加变量,允许代理对内存数据的访问做同步
var value int
go func() {
   memoryAccess.Lock()    //加锁,让goroutint独占该内存的访问权限
   value++
   memoryAccess.Unlock()  //解锁,goroutine使用完了该段内存
} ()
memoryAccess.Lock()    //加锁,声明下面的条件语句独占变量data的内存访问权限
if value==0 {
   fmt.Printf("the value is %v.\n",value)
} else {
   fmt.printf("the value is %v.\n",value)
}
memoryAccess.Unlock()  //解锁,宣布独占结束


4、死锁、活锁和饥饿
死锁:
所有并发进程彼此等待的程序,若无外界干预,程序将永远无法恢复。
示例(因时间问题导致死锁):
type value stuct {
   mu sync.Mutex
   value int
}
var wg sync.WaitGroup
printSum := func(v1,v2 *value) {
   defer wg.Done()
   v1.mu.Lock()   //尝试进入临界区传入一个值
   defer v1.mu.Unlock()   //使用defer语句在printSum返回之前退出临界区

   time.Sleep(2*time.Second)  //休眠一段时间来模拟工作(并触发死锁)
   v2.mu.Lock()
   defer v2.mu.Unlock()

   fmt.Printf("sum=%v\n",v1.value + v2.value)
}
var a,b value
wg.Add(2)
go printfSum(&a,&b)
go printSum(&b,&a)
wg.Wait()
代码结果输出:
fatal error: all goroutines are asleep - deadlock!

出现死锁的条件:
相互排斥:  并发进程同时拥有资源的独占权
等待条件:  并发进程必须同时拥有一个资源,并等待额外的资源
没有抢占:  并发进程拥有的资源只能被该进程释放,即可以满足这个条件
循环等待:  一个并发进程必须等待一系列其他并发进程,而一系列的并发进程同时也在等待最开始的进程,这样便满足了这个最终条件


活锁:
正在注定执行并发操作的程序,但是这些操作无法向前推进程序的状态
示例:
cadence := sync.NewCond(&sync.Mutex{})
go func() {
   for range time.Tick(1*time.Millisecond) {
      cadence.Broadcast()
   }
}()

takeStep := func() {
   cadence.L.Lock()
   cadence.Wait()
   cadence.L.Unlocak()
}

tryDir := func(dirName string,dir *int32,out *bytes.Buffer) bool {
   fmt.Fprintf(out," %v",dirName)
   atomic.AddInt32(dir,1)
   takeStep()
   if atomic.LoadInt32(dir) == 1 {
      fmt.Fprint(out,". Sucess!")
      return true
   }

   takeStep()
   atomic.AddInt32(dir,-1)
   return false
}

var left,right int32
tryLeft := func(out *bytes.Buffer) bool { return tryDir("left",&left,out)}
tryRight := func(out *bytes.Buffer) bool { return tryDir("right",&right,out)}

walk := func(walking *sync.WaitGroup,name string) {
   var out bytes.Buffer
   defer func() { fmt.Println(out.String()) }()
   defer walking.Done()
   fmt.Fprintf(&out,"%v is trying to scoot:",name)
   for i := 0; i < 5; i++ {
      if tryLeft(&out) || tryRight(&out) {
         return
      }
   }
   fmt.Fprintf(&out,"\n%v tosses her hands up in exasperation!",name)
}
var peopleInHallway sync.WaitGroup
peopleInHallway.Add(2)
go walk(&peopleInHallway,"Alice")
go walk(&peopleInHallway,"Barbara")
peopleInHallway.Wait()

示例掩饰了使用活锁的一个十分常见的原因:
两个或两个以上的并发进程试图在没有协调的情况下防止死锁
活锁要比死锁更复杂


饥饿:
在任何情况下,并发进程都无法获得执行工作所需要的所有资源
饥饿通常意味着有一个或多个贪婪的并发进程,它不公平的阻止一个或多个并发进程,以尽可能有效的完成工作,或者阻止全部并发进程
示例(一个贪婪的goroutine,一个平和的goroutine):
示例:
var wg sync.WaitGroup
var sharedLock sync.Mutex
const runtime = 1*time.Second

greedyWorker := func() {
   defer wg.Done()

   var count int
   for begin := time.Now(); time.Sine(begin) <= runtime; {
      sharedLock.Lock()
      time.Sleep(3*time.Nanosecond)
      sharedLock.Unlock()
      count++
   }
   fmt.Printf("Greedy worker was able to execute %v work loops\n",count)
}
politeWorker := func() {
   defer wg.Done()

   var count int
   for begin := time.Now(); time.Sine(begin) <= runtime; {
   sharedLock.Lock()
   time.Sleep(1*time.Nanosecond)
   sharedLock.Unlock()

   sharedLock.Lock()
   time.Sleep(1*time.Nanosecond)
   sharedLock.Unlock()

   count++
   }
   fmt.Printf("Polite worker was able to execute %v work loops.\n",count)
}

wg.Add(2)
go greedyWorker()
go politeWorker()

wg.Wait()



确定并发安全:

面对复杂性的简单性:
go语言的并发,低延GC
go语言运行时也会自动处理并发操作到操作系统线程上。




第二章
对你的代码建模:通信顺序进程

并发与并行的区别:
并发属于代码,并行属于一个运行种程序。
并行是一个时间或者上下文的函数
其他语言并发需要程序按照线程以及对于内存访问之间使用同步来建模
若计算机不能处理太多的线程,需要创建一个线程池并将操作在线程池中复用

什么是CSP:
通信顺序进程
go语言的优势之一是并发

go语言的并发哲学:
CSP一直都是go语言设计的重要组成部分
go语言支持通过内存访问同步和遵循该技术的原语来编写并发代码的传统方式
go语言的并发性哲学总结:
追求简洁,尽量使用channel,并且认为goroutine的使用是没有成本的


第三章
go语言的并发组件

goroutine:
并发函数,与其他代码一起运行。可以在函数之前添加go关键字来触发。
示例:
func main() {
   go sayHello()
   //继续执行自己的逻辑
}
func sayHello () {
   fmt.Println("hello")
}

基于匿名函数的goroutine示例:
go func() {
   fmt.Println("hello")
}()    //在go关键字后面调用匿名函数来使用
//继续执行自己的逻辑

将函数赋给一个变量,并将其命名为匿名函数
sayHello := func() {
   fmt.Println("hello")
}
go sayHello()
//继续执行自己的逻辑

goroutine 不是OS线程,不是绿色线程(由语言运行时管理的线程),是协程,是一种更高级别的抽象
协程:
一种非抢占式的简单并发子goroutine(函数,闭包或方法),他们不能被中断,协程有多个点,允许暂停或重新进入

go语言的独特之处在于他们与狗语言的运行时的深度集成
协程和goroutine都是隐式并发结构,但并发并不是协程的属性

测算在goroutine创建之前后之后分配的内存数量
goroutine不被GC的事实与运行时的自省能力结合

memConsumed := func() uint64 {
   runtime.GC()
   var s runtime.MemStats
   runtime.ReadMemStats(&s)
   return s.Sys
}

var c <-chan interface{}
var wg sync.WaitGroup
noop := func() { wg.Done(); <-c}   //不会退出的goroutine,直到进程结束,用于测算

const numGrouptines = 1e4  //定义要创建goroutine的数量,使用大数定律,接近一个goroutine的大小
wg.Add(numGoroutines)
before := memConsumed()    //测算在创建goroutine之前消耗的内存总量
for i := numGoroutines; i > 0; i-- {
   go noop()
}
wg.Wait()
agter := memConsumed() //测算在创建goroutine之后消耗的总量
fmt.Printf("%.3fkb",float64(after-before)/numGoroutines/1000)

创建两个goroutine并在他们之间发送一条消息
func BenchmarkContextSwitch(b *testint.B) {
   var wg sync.WaitGroup
   begin := make(chan struct{})
   c := make(chan struct{})

   var token struct{}
   sender := func() {
      defer wg.Done()
      <-begin    //等待被告知开始执行,对上下文切换度量时不考虑设置和启动每个goroutine成本
      for i := 0; i < b.N;i++ {
         c <- token //将消息发送到接收器goroutine,做发出信号时候记录时间用
      }
   }
   reciever := func() {
      defer wg.Done()
      <-begin    //等待被告知开始执行,对上下文切换度量时不考虑设置和启动每个goroutine成本
      for i := 0;i < b.N;i++ {
         <-c    //收到消息
      }
   }
   wg.Add(2)
   go sender()
   go receiver()
   b.StartTimer() //开始计时
   close(begin)   告诉俩个goroutine开始运行
   wg.wait()
}

使用一个CPU进行基准测试
go test -bench=. -cpu=1 程序文件路径及名称



sync包
WaitGroup:
不关心并发操作结果,或使用其他方法来收集结果,WaitGroup是等待一组并发操作完成的好方法
还可以使用channel和select语句
使用WaitGroup等待goroutine完成的基本例子:
var wg sync.WaitGroup
wg.Add(1)  //调用Add,参数为1,表示一个goroutine开始
go func() {
   defer wg.Done()    //使用defer关键字来确保在goroutine退出前执行Done操作,向WaitGroup表明已经退出
   fmt.Println("1st goroutine sleeping...")
   time.Sleep(1)
} ()

wg.Add(1)  //调用Add,参数为1,表示一个goroutine开始
go func() {
   defer wg.Done()
   fmt.Println("2nd goroutine sleeping...")
   time.Sleep(2)
}()

wg.Wait()  //执行Wait操作,将阻塞main goroutine,直到所有的goroutine表明他们已经退出
fmt.Println("All goroutines complete.")


hello := func(wg *sync.WaitGroup,id int) {
   defer wg.Done()
   fmt.Printf("Hello from %v!\n",id)
}
const numGreeters = 5
var wg sync.WaitGroup
wg.Add(numGreeters)
for i := 0;i < numGreeters;i++ {
   go hello(&wg,i+1)
}
wg.Wait()


互斥锁和读写锁(Mutex互斥锁)
保护程序临界区中的一种方式,提供了一种安全的方式来访问临界区
临界区是程序中需要独占访问共享资源的区域
channel通过通信共享内存,Mutex通过开发人员的约定同步访问共享内存
可以通过使用Mutex对内存进行保护来协调对内存的访问

两个goroutine试图增加和减少一个共同的值,使用Mutex互斥锁来同步访问:
var count int
vat lock sync.Mutex
increment := func() {
   lock.Lock()
   defer lock.Unlock()
   count++
   fmt.Printf("Incrementinf:%d\n",count)
}

decrement := func() {
   lock.Lock()    //请求对临界区独占(计数器),使用互斥锁来解决
   defer lock.Unlock()    //指出已经完成对临界区锁定的保护
   count--
   fmt.Printf("Decrementing: %d\n",count)
}

//增量
for i := 0;i <= 5;i++ {
   arithmetic.Add(1)
   go func() {
      defer arithmetic.Done()
      decrement()
   }()
}

//减量
for i := 0;i <= 5;i++ {
   arithmetic.Add(1)
   go func() {
      defer arithmetic.Done()
      decrement()
   }()
}

arithmetic.Wait()
fmt.Println("Arithmetic complete.")

不同类型的互斥对象   sync.RWMutex
RWMutex对内存有着更多的控制,如读锁,写锁
producer := func(wg *sync.WaitGroup,l sync.Locker) {   //l参数是sync.Locker类型,接口方法Lock和Unclock分别对应Mutex和RWMutex
   defer wg.Done()
   for i := 5;i>0;i-- {
      l.Lock()
      l.Unlock()
      time.Sleep(1)  //让producer等待1s,使其比观察者goroutines更不活跃
   }
}

observer := func(wg *sync.WaitGroup,l sync.Locker) {
   defer wg.Done()
   l.Lock()
   defer l.Unlock()
}

test := func(count int,mutex,rwMutex sync.Locker) time.Duretion {
   var wg sync.WaitGroup
   wg.Add(count+1)
   beginTestTime := timeNow()
   go producer(&wg,mutex)
   for i:=count;i>0;i-- {
      go observer(&wg,rwMutex)
   }
   wg.Wait()
   return time.Since(beginTestTime)
}

tw := tabwriter.NewWriter(os.Stdout,0,1,2,' ',0)
defer tw.Flush()

var m sync.RWMutex
fmt.Fprintf(tw, "Readers\ttRWMutext\tMutext\n")
for i:=0;i<20;i++ {
   count := int(math.Pow(2,float64(i)))
   fmt.Fprintf(
      tw,
      "%d\t%v\t%v\n",
      count,
      test(count,&m,m.Rlocker()),
      test(count,&m,&m),
   )
}


cond
一个goroutine的集合点,等待或发布一个event
event是两个或两个以上的goroutine之间的任意信号、
cond让goroutine有效的等待信号
示例:
c := sync.NewCond(&sync.Mutex{})   //创建新的cond,NewCond创建一个类型,满足sync.Locker接口,使cond类型以并发安全的方式与goroutine协调
c.L.Lock() //锁定条件
for conditionTrue() == false {
   c.Wait()   //等待通知,阻塞通信,goroutine将被暂停
}
c.L.Unlock()   //解锁
Wait阻塞挂起goroutine,允许其他goroutine在OS线程上运行


示例:
c := sync.NewCond(&sync.Mutex{})   //使用sync.Mutex作为锁
queue := make([]interface{},0,10)  //创建长度为0切片,用10的容量实例化
removeFromQueue := func(delay time.Duration) {
   tiem.Sleep(delay)
   c.L.Lock() //再次进入临界区,以便可以修改与条件相关的数据
   queue = queue[1:]  //通过将切片的头部重新分配到第二个项目来模拟对一个项目的排队
   fmt.Println("Removed from queue")
   c.L.Unlock()   //退出条件的临界区,已经成功删除了一个项目
   c.Signal() //让等待的goroutine知道发生的事
}
for i:=0;i<10;i++{
   c.L.Lock() //在条件锁存器上调用锁来进入临界区
   for len(queue) == 2 {  //检查循环队列长度
      c.Wait()   //调用Wait,将暂停main goroutine直到一个信号的条件已经发送
   }
   fmt.Println("Adding to queue")
   queue = append(queue,struct{}{})
   go removeFromQueue(1*time.Second)  //创建新的goroutine,将在一秒后删除一个元素
   c.L.Unlock()   //退出临界区,成功进入一个项目
}


示例:
type Button struct {   //定义一个Button类型,包含一个条件Clicked
   Clicked *sync.Cond
}
button := Button{ Clicked: sync.NewCond(&sync.Mutex{})}

subscribe := func(c *sync.cond,fn func()) {    //定义一个便利构造函数,程序在自己的goroutine上处理来自条件的信号
   var goroutineRunning sync.WaitGroup
   foroutineRunning.Add(1)
   go func() {
      goroutineRunning.Done()
      c.L.Lock()
      defer c.L.Unlock()
      c.Wait()
      fn()
   }()
   goroutineRunning.Wait()
}

var clickRegistered sync.WaitGroup //鼠标按键设置的处理程序,反过来调用Cond上Broadcast,
clickRegistered.Add(3)
subscribe(button.Clicked,func() {  //创建一个WaitGroup,确保程序在写入stdout之前不会退出
   fmt.Println("Maxinmizing window.")
   clickRegistered.Done()
})
subscribe(button.Clicked,func() {  //注册一个程序,当单击按键时,将模拟最大化按钮的窗口
   fmt.Println("Displaying annoying dialog box")
   clickRegistered.Done()
})
subscribe(button.Clicked,func() {  //注册一个处理程序,在单击鼠标时模拟显示对话框
   fmt.Println("Mouse clicked.")
   clickRegistered.Done()
})

button.Clicked.Broadcast() //模拟一个用户通过单击应用程序的按钮来单击鼠标按键

clickRegistered.Wait()




once:
函数只能调用一次
示例:
var count int
increment := func() {
   count++
}
var once sync.Once

var increments sync.WaitGroup
increments.Add(100)
for i:=0;i<100;i++{
   go func() {
      defer increments.Done()
      once.Do(increment)
   }()
}

increments.Wait()
fmt.Printf("Count is %d\n",count)


sync.Once只计算Do方法的次数
示例:
var count int
increment := func() { count++ }
decrement := func() { count-- }

var once sync.Once
once.Do(increment)
once.Do(decrement)
fmt.Printf("Count: %d\n",count)

本书作者带你一步一步深入这些方法。你将理解 Go语言为何选定这些并发模型,这些模型又会带来什么问题,以及你如何组合利用这些模型中的原语去解决问题。学习那些让你在独立且自信的编写与实现任何规模并发系统时所需要用到的技巧和工具。 理解Go语言如何解决并发难以编写正确这一根本问题。 学习并发与并行的关键性区别。 深入到Go语言的内存同步原语。 利用这些模式中的原语编写可维护的并发代码。 将模式组合成为一系列的实践,使你能够编写大规模的分布式系统。 学习 goroutine 背后的复杂性,以及Go语言的运行时如何将所有东西连接在一起。 作者简介 · · · · · · Katherine Cox-Buday是一名计算机科学家,目前工作于 Simple online banking。她的业余爱好包括软件工程、创作、Go 语言(igo、baduk、weiquei) 以及音乐,这些都是她长期的追求,并且有着不同层面的贡献。 目录 · · · · · · 前言 11并发概述 9 摩尔定律,Web Scale和我们所陷入的混乱 10 为什么并发很难? 12 竞争条件 13 原子性 15 内存访问同步 17 死锁、活锁和饥饿 20 确定并发安全 28 面对复杂性的简单性 31 第2章 对你的代码建模:通信顺序进程 33 并发与并行的区别 33 什么是CSP 37 如何帮助你 40 Go语言并发哲学 43 第3章 Go语言并发组件 47 goroutine 47 sync包 58 WaitGroup 58 互斥锁和读写锁 60 cond 64 once 69 池 71 channel 76 select 语句 92 GOMAXPROCS控制 97 小结 98 第4章 Go语言并发模式 99 约束 99 for-select循环103 防止goroutine泄漏 104 or-channel 109 错误处理112 pipeline 116 构建pipeline的最佳实践 120 一些便利的生成器 126 扇入,扇出 132 or-done-channel 137 tee-channel 139 桥接channel模式 140 队列排队143 context包 151 小结 168 第5章 大规模并发 169 异常传递169 超时和取消 178 心跳 184 复制请求197 速率限制199 治愈异常的goroutine 215 小结 222 第6章 goroutine和Go语言运行时 223 工作窃取223 窃取任务还是续体 231 向开发人员展示所有这些信息 240 尾声 240 附录A 241
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值