Golang 协程

Golang协程

Do not communicate by sharing memory; instead, share memory by communicating.

通信共享内存—channel

channel,管道,可以理解为队列,用于goroutine之间传递指定类型的值来同步运行和通讯,通道有几种类型:

分类方式种类
通信方向双向,仅接收,仅发送
有无缓冲(buffer)无缓冲,有缓冲
  1. 按照通信方向区分:
chan time.Time    // 双向,可用来发送和接收time.Time类型的值
chan<- float64    // 仅发送,仅可用来发送float64类型的值(向channel发送)
<-chan int        // 仅接收,仅可用来接收int类型的值(从channel接收)
  1. 按照有无缓冲区分:
nbc := make(chan int)       // 不带缓冲的int类型管道
bc := make(chan int, 10)    // 带缓冲的int类型管道
// 获取channel容量
n := cap(bc)  // n = 10
// 获取channel中的元素数量
l := len(bc)  // l = 0

管道的使用

  1. 数据的发送与接收

从缓存channel中取数据是无序的

nbc <- 3         // 往管道nbc发送3
i := <-bc        // 从管道bc接收一个值,根据上例,类型为int
// 若channel 不带buffer,或容量已满,向其发送数据时,发送方会阻塞
// 若channel 为空,从其接收数据时,接收方或阻塞
  1. 关闭管道

close(ch),channel关闭意味着数据的写入(或生产)已经完成,此时不能再向该channel发送数据,否则会panic,但可以从中读取数据,即使channel已空,读取也不会panic,只是返回其数据类型的0值

注意:关闭已关闭的或 len == 0 的channel时, 会panic;关闭只读通道无法编译成功

ch := make(chan string)
go func() {
    ch <- "Hello!"
    close(ch)
}()
v, ok := <-ch         // 变量v的值为空字符串"",变量ok的值为false
fmt.Println(v, ok)    // 输出Hello! true
fmt.Println(<-ch)     // 输出零值 - 空字符串"",不会阻塞
v, ok = <-ch          // 变量v的值为空字符串"",变量ok的值为false
fmt.Println(v, ok)    // 输出"" false

循环从channel中接收数据, for range chan 会在channel 关闭后自动退出循环:

ch := make(chan string)

// 不关心收到的值的情况
ticker := time.NewTicker(time.Second)
for range ticker.C{
	fmt.Println("1 second pass")
}
// 需要收到的值的情况
for v := range ch{
	fmt.Println(v)
}
// 等同于
for {
	if v, ok := <-ch;ok{
		fmt.Println(v)
	}else{
}

  1. 无缓冲和有缓冲channel的应用

无缓冲的channel 常用于信号量的发送(https://play.golang.org/p/GpzSJqZQbHr)

var ch = make(chan struct{}) // 使用struct{},表示不在乎信息内容,且struct{}占用内存为0

// 假设Add是一个很费时的io操作
func Add(a, b int) {
	time.Sleep(5 * time.Second)
	fmt.Printf("%d+%d=%d\n", a, b, a+b)
	ch <- struct{}{}
}

func main() {
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			go Add(i, j)
		}
	}
	// 100加法个全部计算后退出
	count := 0
	for {
		if _, ok := <-ch; ok {
			count++
		}
		if count == 100 {
			fmt.Println("all job done!")
			break
		}
	}
}

有缓冲的channel 用于生产者消费者模型(cap = 1)或数据临时存放(cap > 1)
生产消费模型的channel buffer 最好设为1,即生产和消费的速度相当或消费快于生产,否则即使调高buffer,也会产生任务积压

// 生产者消费者模型
type Adder struct{
	Num [2]int
	Sum int
}
var bf = make(chan Adder,1)
var r = rand.New(rand.NewSource(time.Now().Unix()))
// 生产者
func produce(){
	num := [2]int{r.Intn(100),r.Intn(100)}
	bf <- Adder{
		Num: num,
	}
}

//消费者
func consume(ad Adder) Adder{
	ad.Sum = ad.Num[0]+ad.Num[1]
	return ad
}

func main(){
	go func(){
		for{
			time.Sleep(time.Second)
			produce()
		}
	}()
	for v := range bf{
		go func(t Adder){
			temp:= consume(t)
			fmt.Println(temp)
		}(v)
	}
}

数据临时存放(类似slice,只是无需锁操作,效率更高)

// 场景:获取一月的平均气温,但接口只支持日查询
// 此处需用到sync.WaitGroup,是Go语言标准库的一部分,用于等待一组goroutine结束运行
type Data struct{
		Date time.Time
		Temp float64
}

func main(){
	start := time.Date(2020,5,1,0,0,0,0,time.Local)
	end := time.Date(2020,5,31,0,0,0,0,time.Local)
	wg := sync.WaitGroup{}
	ch := make(chan Data, 31)

	for ;start.Before(end);start = start.AddDate(0,0,1){
		wg.Add(1)
		go func(t time.Time){
			defer wg.Done()
			data := fetchAPI(t)
			ch<- data
		}(start)	
	}
	// 等待5月所有日期查询完毕
	wg.Wait()

	var avgTemp float64
	for v := range ch{
		aveTemp += v.Temp
	}
	avgTemp /=31
	fmt.Println("avg temp of May = ", avgTemp)
}

func fetchAPI(date time.Time) Data{
		data := get("url", date)
		return data
}

协程—goroutine

Golang中使用go 关键字,即可开启一个goroutine,常用的有两种方式:

go Add(1,2)
// go func(形参){函数体}(实参),必须有实参部分
go func(a,b int){
	fmt.Printf("%d+%d=%d\n", a, b, a+b)
}(1,2)

在循环中使用匿名函数的方式创建goroutine,使用不当会出现很多问题,例如:

func main(){
	for i:= 0; i< 5; i++{
		go func(){
			fmt.Println(i)
		}()
	}
	time.Sleep(time.Second)
}

// Output:
// go vet 会有如下报警:
// ./prog.go:16:16: loop variable i captured by func literal
5
5
5
5
5

// 循环中创建协程后,当匿名函数执行时,i的值已累加至5,故输出的值都为5,正确写法如下
func main(){
	for i:= 0; i< 5; i++{
		go func(t int){
			fmt.Println(t)
		}(i)
	}
	time.Sleep(time.Second)
}
// 或
func main(){
	for i:= 0; i< 5; i++{
		t := i
		go func(){
			fmt.Println(t)
		}()
	}
	time.Sleep(time.Second)
}

// 延伸:闭包

recover

在Golang中,任意一个goroutine 中发生panic并且未被recover捕获,整个程序都会停止运行,故需在可能panic的goroutine中按需recover panic

// 无recover(https://play.golang.org/p/V1vgZh4hCCK)
func main() {
	// 协程1
	go func() {
		for {
			fmt.Println("I am doing something here.")
			time.Sleep(time.Second)
		}
	}()
	// 协程2
	go func() {
		for i := 10; i > (-10); i-- {
			fmt.Println(5/i)
		}
	}()
	time.Sleep(10 * time.Second)
}
// 当i循环至0时,5/i panic,主协程退出,程序运行

// 使用recover 捕获panic(https://play.golang.org/p/3TLy6PPCnxP)
func main() {
	go func() {
		for {
			fmt.Println("I am doing something here.")
			time.Sleep(time.Second)
		}
	}()
	go func() {
		defer func() {
			if err := recover();err != nil{
				log.Println(err)
			}
		}()
		for i := 10; i > (-10); i-- {
			fmt.Println(5/i)
		}
	}()
	time.Sleep(10 * time.Second)
}

lock

Golang中channel的所有操作都是协程安全的,不需要加锁。但也提供了锁工具,类似Java的synchronized(Golang中的map, slice等都不是协程安全的)

//以slice为例
func main() {
	raceSlice()
}

func raceSlice(){
	x := make([]int, 0)

	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i:=0;i<100;i++{
			x = append(x, i)
		}
	}()
	go func() {
		defer wg.Done()
		for i:=0;i<100;i++{
			x = append(x, i)
		}
	}()
	wg.Wait()
	fmt.Println(len(x))
}

// 预期结果为200,但实际结果为<=200的随机数,不加锁写slice时会导致数据丢失

// map与此类似,并发读写、并发写都会导致panic
func raceMap(){
	m := make(map[int]int)
	go func() {				//开一个协程写map
		for i := 0; i < 100; i++ {
			m[i] = i
		}
	}()

	go func() {				//开另一个协程读map
		for i := 0; i < 100; i++ {
			m[i] = i
		}
	}()

	time.Sleep(time.Second * 20)
}

并发读写时产生数据竞争(竞态 race)

go build, go run, go test 命令都可添加 —race 参数,检测竞态

lock的分类和用法

lock分为互斥锁(sync.Mutex)和读写锁(sync.RWMutex)

Mutex, 互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景

注意点:

  1. Unlock一个未加锁的锁,会导致运行时错误
  2. Lock一个已加锁的锁,会导致死锁
// 上面例子的并发写slice,加锁
func raceSlice(){
	x := make([]int, 0)

	wg := sync.WaitGroup{}
	lock := sync.Mutex{} //lock在使用后不能复制,故在需要传递锁的情景下需传递指针
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i:=0;i<100;i++{
			lock.Lock()
			x = append(x, i)
			lock.Unlock()
		}
	}()
	go func() {
		defer wg.Done()
		for i:=0;i<100;i++{
			lock.Lock()
			x = append(x, i)
			lock.Unlock()
		}
	}()
	wg.Wait()
	fmt.Println(len(x))
}

// Output: 200

sync.RWMutex 读写锁的规则:

  1. 锁写时,只有获得写锁的goroutine可以写,其他不能读也不能写
  2. 锁读时,其他goroutine可以读,但不能写
// 规则2 没有锁写时可以多个goroutine同时读
var m *sync.RWMutex = &sync.RWMutex{}

func main() {
    // 多个同时读
    go read(1)
    go read(2)

    time.Sleep(2*time.Second)
}

func read(i int) {
    println(i,"start reading")

    m.RLock()
    println(i,"reading")
    time.Sleep(1*time.Second)
		m.RUnlock()

    println(i,"read over")
}
// Output 
// 1 start reading
// 1 reading
// 2 start reading   (1没读完时,2已经开始读)
// 2 reading
// 1 read over
// 2 read over
var m *sync.RWMutex = &sync.RWMutex{}
func main() {
	// 写的时候不能
	go write(1)
	go read(2)
	go write(3)

	time.Sleep(2*time.Second)
}

func read(i int) {
    println(i,"start reading")

    m.RLock()
    println(i,"reading")
    time.Sleep(1*time.Second)
		m.RUnlock()

    println(i,"read over")
}

func write(i int) {
	println(i,"start writing")

	m.Lock()
	println(i,"writing")
	time.Sleep(1*time.Second)
	m.Unlock()

	println(i,"write over")
}

//Output: 
// 1 start writing
// 1 writing
// 2 start reading(等待写锁解锁)
// 3 start writing
// 1 write over(写锁解锁)
// 2 reading
// 2 read over(读锁解锁)
// 3 writing (等待读锁解锁)

lock 也可以匿名嵌入到其他结构体中使用

type SomeReader struct{
	Source string
	*sync.Mutex
}

func(s SomeReader) Read(){
	s.Lock()
	...
	s.Unlock()
}

sync/atomic

atomic 包提供原子操作,如数字的载入、增减、比较、交换、存储等,无序加锁,操作更简单。主要用途在于在协程中对数字进行操作。

func main() {
	var n int32
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			n++  // 多个协程同时操作n,产生竞态
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(n) 
}
// Output: 966(<=1000的数)

func main(){
	var n int32
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			atomic.AddInt32(&n, 1)
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(n) // 在协程中读取时,使用atomic.LoadInt32(&n)
}
// Output: 1000

sync

sync包提供编写并发安全代码的工具,包括同步锁,读写锁,WaitGroup,条件变量Cond,Once,协程安全的Map, 对象池Pool等

Cond

func main() {
   lock := &sync.Mutex{}
   cond := sync.NewCond(lock)
   wg := sync.WaitGroup{}
   for i := 0; i < 5; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         cond.L.Lock()  // 使用cond前必须加锁
         defer cond.L.Unlock()
         cond.Wait()
         fmt.Println("条件满足")
      }()
   }
   wg.Add(1)
   go func() {
      defer wg.Done()
      for i := 0; i < 5; i++ {
         time.Sleep(time.Second)
         if i == 4 {
            cond.L.Lock()
            //defer cond.L.Unlock()  // 循环中不要使用defer
            //cond.Signal() // 唤醒一个等待中的goroutine,此例子中,Signal 会唤醒5个等待协程中的1个,其余4个无法唤醒,死锁
            cond.Broadcast() // 唤醒所有等待中的goroutine
            cond.L.Unlock()
         }
      }
   }()
   wg.Wait()
}

Once

once可以保证函数在多个goroutine中只执行一次

func main() {
	wg := sync.WaitGroup{}
	once := sync.Once{}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(t int) {
			defer wg.Done()
			once.Do(func() {
				fmt.Println("run once")
			})
			fmt.Println(t)
		}(i)
	}
	wg.Wait()
}

Pool实现对象池,在需要频繁创建销毁对象的地方使用对象池,可以降低GC的压力,提高程序性能

Map实现了协程安全的哈希表

select

select用于从一组可能的通讯中选择一个进一步处理。如果任意一个通讯都可以进一步处理,则从中随机选择一个,执行对应的语句。否则,如果又没有默认分支(default case),select语句则会阻塞,直到其中一个通讯完成。

func main() {
	ch := make(chan int, 1)
	s := make(chan struct{})
	go write(ch)
	go func() {
		timer := time.NewTimer(time.Second * 5)
		select {
		case val:= <-ch:  // 在5s内从ch收到了值
			fmt.Println("receive before timeout, value is ", val)
			s<- struct{}{}
		case <-timer.C:   // 在5s内未从ch收到值
			fmt.Println("timeout")
			s<- struct{}{}
		}
	}()
	<-s
}
func write(c chan<- int){
	r := rand.New(rand.NewSource(time.Now().Unix()))
	time.Sleep(time.Duration(r.Intn(10)) * time.Second)
	c<- 1
}

select 可以进行嵌套,以处理同时收到数据时代码执行的优先级问题

下面代码,若highChan和lowChan 同时收到数据,第一段代码会随机处理,第二段代码必会先处理高优先级(handleHigh)的数据。

// 1. 无优先级
for {
    select {
    case data := <- highChan:
        handleHigh(data)
    case data := <- lowChan:
        handleLow(data)
    }
}
// 2. 无优先级
for {
    select {
    case data := <- highChan:
        handleHigh(data)
    default:
        select {
        case data := <- highChan:
            handleHigh(data)
        case data := <- lowChan:
            handleLow(data)
        }
    }
}

参考文章

  1. https://www.bookstack.cn/read/golang101/concurrent.md
  2. http://blog.xiayf.cn/2015/05/20/fundamentals-of-concurrent-programming/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值