Go协程与协程池

Posted 沈子恒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go协程与协程池相关的知识,希望对你有一定的参考价值。

1. Golang协程

  • golang和其它语言最大区别莫过于goroutine,也就是go的协程,example如下:
package main

import "fmt"
import "time"

func go_worker(name string) 
	for i:=0; i<10; i++ 
		fmt.Println("this is go worker :" , name)
	


func main()
	go go_worker("lineshen")
	go go_worker("glorialu")

	time.Sleep(time.Second*5) // print effective
	fmt.Println("main finished")

输出:

this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
main finished

如果不用协程,上面代码应该三个线程运行,分别是主函数main和 两个函数。 使用协程之后,实际运行两个线程,这就是并发的好处。

Note当使用go启动协程之后,这2个函数就被切换到协程里面执行了,但是这时候主线程结束了,这2个协程还没来得及执行就会挂了!所以不在main中加time进行sleep就无法看到协程中的执行结果。

  • 多次运行,其结果可能如下:
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : glorialu
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
this is go worker : lineshen
main finished

会发现lineshen和 glorialu 的顺序不是固定的,这进一步说明了一个问题,那就是多个协程是同时执行的。不过睡眠这种做法肯定是不靠谱的,go 自带一个WaitGroup可以解决这个问题,。

  • WaitGroup示例如下:
package main

import ("fmt"
		"time"
		"sync")
var wg sync.WaitGroup

func go_worker(name string) 
	for i:=0; i<10; i++ 
		fmt.Println("this is go worker :" , name)
	
	wg.Done()


func main()
	wg.Add(2)
	go go_worker("lineshen")
	go go_worker("glorialu")
	wg.Wait()
	
	// time.Sleep(time.Second*5) // print effective
	fmt.Println("main finished")

sync.WaitGroup也起到了time.Sleep的效果。但是协程内部的执行顺序是不确定的

sync.WaitGroup用法: var 是声明了一个全局变量 wg,类型是sync.WaitGroup,wg.Add(2) 指2个goroutine需要执行,
wg.Done 相当于 wg.Add(-1) 意思就是该协程执行结束;wg.Wait()通信主线程wait,等2个协程都执行完再退出

应用实例:从3个库取不同的数据汇总处理,最简单写法就是查3次库,但是这3次查询必须按顺序执行,大部分编程语言的代码执行顺序都是从上到下,假如一个查询耗时1s,3个查询就是3s,但是使用协程你可以让这3个查询同时进行,也就是1s就可以搞定。

2. Golang协程池与channel机制

  • 同类型channel

如何将协程中结果返回给主线程?golang提供了channel机制。示例如下:

package main

import ("fmt"
		"sync")
var wg sync.WaitGroup

func go_worker(name string, ch chan string) 
	for i:=0; i<10; i++ 
		ch <- name
	
	wg.Done()


func main()
	wg.Add(2)
	ch := make(chan string)
	go go_worker("lineshen", ch)
	go go_worker("glorialu", ch)
	for 
		fmt.Println("this is go worker :" , <-ch)
		
	wg.Wait()
	
	fmt.Println("main finished")

代码执行结果如下。就是实例化了一个channel,go启动的协程同时向这个2个管道输出数据,主线程使用了一个for循环从管道里面取数据。其实就是一个生产者-消费者模式,与redis队列有点像。下面也可以看到lineshen和glorialu进入管道的顺序是不固定的。如果实验发现输出顺序固定的,那是因为电脑跑的太快了,可以把循环开的大一点。

this is go worker : glorialu
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : lineshen
fatal error: all goroutines are asleep - deadlock!

也看到发生了致命的错误:fatal error: all goroutines are asleep - deadlock! 所有的协程都睡眠了,程序监测到死锁!go的channel默认是阻塞的(假如不设置缓存),需要“放一个-取一个”。如果channel里面放置的数据没有取出来,程序就会一直等下去,死锁了;同时,如果channel里面没有放置数据,主线程去取数据也会发生死锁!

如何解决这个问题呢?标准的做法是主动关闭管道,或者知道应该什么时候关闭管道。对于示例代码,可以简单加入一个计数器判断何时channel中的数据可以完全被取出。如下:

package main

import ("fmt"
		"sync")
var wg sync.WaitGroup

func go_worker(name string, ch chan string) 
	for i:=0; i<10; i++ 
		ch <- name
	
	wg.Done()


func main()
	wg.Add(2)
	ch := make(chan string)
	go go_worker("lineshen", ch)
	go go_worker("glorialu", ch)
	counter := 1
	for 
		fmt.Println("this is go worker :" , <-ch)
		
		if counter >= 20 
			close(ch)
			break
		
		counter++
	
	wg.Wait()
	
	fmt.Println("main finished")

输出如下:

this is go worker : glorialu
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : glorialu
this is go worker : lineshen
this is go worker : lineshen
main finished
  • 多种类型channel,使用select进行判断
package main

import ("time"
	"fmt")

func go_worker1_int(ch chan int) 
	for i:=0; ; i++ 
		ch <- i
		time.Sleep(time.Duration(time.Second))
	


func go_worker2_string(name string, ch chan string)
	for i:=0; ; i++ 
		ch <- name
		time.Sleep(time.Duration(time.Second))
	

func go_worker3_print(ch1 chan int, ch2 chan string)
	chRate := time.Tick(time.Duration(time.Second))
	for 
		select 
		case v := <-ch1:
			fmt.Printf("Received on channel 1: %d\\n", v)
		case v := <-ch2:
			fmt.Printf("Received on channel 2: %s\\n", v)
		case <-chRate:
			fmt.Printf("log...\\n")
			
	


func main()
	ch1 := make(chan int)
	ch2 := make(chan string)
	go go_worker1_int(ch1)
	go go_worker2_string("lineshen", ch2)
	go_worker3_print(ch1, ch2)
	time.Sleep(time.Duration(time.Second*10))
	fmt.Println("main finished")

输出结果如下:

Received on channel 1: 0
Received on channel 2: lineshen
Received on channel 1: 1
Received on channel 2: lineshen
log...
Received on channel 2: lineshen
Received on channel 1: 2
log...
log...
Received on channel 1: 3
Received on channel 2: lineshen
log...
Received on channel 1: 4
Received on channel 2: lineshen
log...

该程序建立了2个管道一个传输int,一个传输string,同时启动了3个协程,前2个协程非常简单,就是每隔1s向管道输出数据,第三个协程是不停的从管道取数据,并通过定时器功能可以每隔一段时间向管道输出内容!

不同的是,go_worker1_int和go_worker2_string是2个不同的管道,通过select可以实现在不同管道之间切换,哪个管道有数据就从哪个管道里面取数据,如果都没数据就等着。

以上是关于Go协程与协程池的主要内容,如果未能解决你的问题,请参考以下文章

swoole协程与go协程

Python中的协程与asyncio原理

Python中的协程与asyncio原理

Python中的协程与asyncio原理

2020-08-20:GO语言中的协程与Python中的协程的区别?

Go语言 | 协程池的应用(可能是全网最适合小白的教程)