等待组


  • 在此之前,我们让主协程等待子协程结束的方式都是睡眠,睡足子协程需要的时间,这种方式显然是不理想的!
  • 等待组(sync.WaitGroup)的原理是:每增加一个子协程,就向等待组中+1,每结束一个协程,就从等待组中-1,主协程会阻塞等待直到组中的协程数等于0为止;
  • 这种方式可以令主协程恰好结束在最后一个子协程结束的时间点上,Perfect!

并发技术4:同步调度_信号量

互斥锁案例1


  • 在很多情境中,数据是不允许并发修改的;
  • 典型的案例如银行账户,银行卡在存取的过程中,存折是不允许在同一时间进行存取操作的,例如卡刚刚取走500,在查询余额时恰好存折又存入500,银行卡在查询余额时会误以为银行并没有扣款,这显然是应该避免的!
  • 所以我们不允许银行卡和存折并发地执行存取操作,必须同步串行有先后地执行存取,这样才不会带来脏读和幻读;
  • 我们可以通过抢互斥锁(sync.Mutex)的方式来强制存取操作同步;
  • 互斥锁的原理是:对于有必要强制同步串行的任务,我们规定它只有得到互斥锁才有执行权,而全局只有一把互斥锁,谁先抢到谁就获得任务执行权,任务进行的过程中如果有其它协程想要得到执行权,它必须阻塞等待至当前任务协程释放同步锁;
  • 下面的例子中,银行卡无论谁先抢到资源锁,都立刻对同步锁进行锁定(mt.Lock()),在其存取操作没有结束之前,另一方必须阻塞等待直至前者将互斥锁释放(mt.Unlock());

package main

import (
"fmt"
"time"
"sync"
)

func main() {

//必须保证并发安全的数据
type Account struct {
money float32
}

var wg sync.WaitGroup
account := Account{1000}
fmt.Println(account)

//资源互斥锁(谁抢到锁,谁先访问资源,其他人阻塞等待)
//全局就这么一把锁,谁先抢到谁操作,其他人被阻塞直到锁释放
var mt sync.Mutex

//银行卡取钱
wg.Add(1)
go func() {
//拿到互斥锁
mt.Lock()

//加锁的访问
fmt.Println("取钱前:",account.money)
account.money -= 500
time.Sleep(time.Nanosecond)
fmt.Println("取钱后:",account.money)
wg.Done()

//释放互斥锁
mt.Unlock()
}()

//存折存钱
wg.Add(1)
go func() {
//拿到互斥锁(如果别人先抢到,则阻塞等待)
mt.Lock()

fmt.Println("存钱前:",account.money)
account.money += 500
time.Sleep(time.Nanosecond)
fmt.Println("存钱后:",account.money)
wg.Done()

//释放互斥锁
mt.Unlock()
}()

wg.Wait()
}

互斥锁案例2


  • 在上面的例子中,银行卡和存折的存取操作,必须强制同步,否则会形成数据的脏读或幻读;
  • 但如果是查询上个月的银行流水或者仅仅是查询用户名之类的只读操作,则没有强制同步的必要,完全可以并发执行;
  • 于是我们对上面的例子稍作修改,使得对银行账户的强制同步仅限于存取操作,而对于其他操作则放开权限令其可以被并发地执行;
  • 原理很简单:没有必要强制同步的任务,不去抢互斥锁就是了——需要确保同步的任务就先抢锁后执行,其余的则不去抢锁,直接执行;

package main

import (
"sync"
"fmt"
"time"
)

//必须保证并发安全的数据
type Account struct {
name string
money float32

//定义该数据的互斥锁
mt sync.Mutex
}

//本方法不能被并发执行——并发安全的
func (a *Account) saveGet(amount float32) {
//先将资源锁起来
a.mt.Lock()

//执行操作
fmt.Println("操作前:", a.money)
a.money += amount
fmt.Println("操作后:", a.money)
<-time.After(3 * time.Second)

//释放资源
a.mt.Unlock()
}

//本方法可以被并发执行——不是并发安全的,无此必要
func (a *Account) getName() string {
return a.name
}

func main() {
a := Account{name: "张全蛋", money: 1000}

var wg sync.WaitGroup

wg.Add(1)
go func() {
//调用一个加锁的方法(同步)
a.saveGet(500)
wg.Done()
}()

wg.Add(1)
go func() {
//调用一个加锁的方法(同步)
a.saveGet(-500)
wg.Done()
}()

for i:=0;i<3 ;i++ {
wg.Add(1)
go func() {
//调用一个普通的没有访问锁的方法(异步)
fmt.Println(a.getName())
wg.Done()
}()
}

wg.Wait()
}

通过信号量控制并发数


  • 控制并发数属于常用的调度;
  • 我们的做法是:规定并发执行的任务都必须先在某个监视管道中进行注册,而这个监视管道的缓存能力是固定的,比如说5,那么注册在该管道中的并发能力就是5;

package main

import (
"fmt"
"time"
"sync"
)

/*信号量:通过控制管道的“带宽”(缓存能力)控制并发数*/

func main() {

//定义信号量为5“带宽”的管道
sema = make(chan int, 5)

var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(index int) {
ret := getPingfangshu(index)
fmt.Println(index, ret)
wg.Done()
}(i)
}
wg.Wait()
}

//该函数只允许5并发执行
var sema chan int
func getPingfangshu(i int) int {
sema <- 1
<-time.After(2 * time.Second)
<- sema
return i
}

学院Go语言视频主页

[清华团队带你实战区块链开发]


扫码获取海量视频及源码 QQ群:721929980