协程
前言
在很多语言中都会提到并发的概念, 例如python, Java,C++等等,一般来说 都会
使用多线程或多进程来 实现并发调度,但是多线程/进程 一般会耗费大量内存, 而在go
语言中我们可以使用协程来达到并发调度的目的,
协程的简介
协程是轻量级的线程(或者说是用户态的线程),与常用的线程不同的是,它本身的调度不由操作系统
来完成,而是由go语言本身所带的调度器来完成,进而减少了上下文切换的开销,这也是go语言性能提升的原因。
协程的简单使用
与其他语言不同的是,go语言中协程的实现非常简单,只需要关键字go就可以实现一个协程,如下:
package main import "fmt" func synchello() { fmt.Println("hello world") } func main() { go synchello() }
在这里我们就利用了go关键字实现了一个简单的协程,但是上面的代码没有任何的输出,
因为虽然我们利用go关键字已经创建出了子协程,但是由于我们的父协程(main函数)已经
结束了,所以子协程也就无法执行了,所以如果我们想让子协程执行,那么我们就需要让父
协程在子协程结束之前先不要结束,如下:
package main import ( "fmt" "time" ) func synchello() { fmt.Println("hello world") } func main() { go synchello() time.Sleep(10 * time.Second) }
这样我们就可以看到子协程的输出了,但是这种方法并不是最佳的比如下面这个代码:
package main import ( "fmt" "time" ) func synchello() { fmt.Println("hello world") } func main() { for i := 0; i <= 20; i++ { go func() { fmt.Println(i) }() } time.Sleep(30 * time.Second) }
输出结果为:
10 15 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21
可以看到,我们并没有按照我们期望的顺序输出,事实上在并发编程上,我们使用Sleep
函数并不是一种高明的手段,Go中给我们提供了很多并发控制的手段,例如:
- channel: 管道,用于协程之间的通信
- Context: 上下文,用于协程之间的通信
- WaitGroup: 信号量
- Mutex: 互斥锁
- RWMutex: 读写互斥锁
我们可以利用它们来实现我们所想要的并发控制
WaitGrouop(信号量)
WaitGroup的介绍及使用
什么是Waitgroup
WaitGroup是Go语言中一个并发控制工具,它由sync
包提供,WairGroup
即等待执行,
我们可以利用它轻易的实现等待一组协程的效果
WaitGroup的结构
Waitgroup向外暴露了三个方法:
- Add:该方法指明了需要我们等待线程的数量
func (wg *WaitGroup) Add(delta int)
- Done:该方法表示一个线程已经执行完毕,当所有的线程都执行完毕之后,Wait方法
才会返回
func (wg *WaitGroup) Done()
- Wait:该方法表示等待所有的线程执行完毕
func (wg *WaitGroup) Wait()
WaitGroup的使用
WaitGroup
的使用非常简单,它的内部实现主要是基于计数器+信号量
,程序刚开始时
调用Add
来初始化计数,每当一个协程执行完毕后调用Done
,这时计数就-1,直到减为
0,在这期间Wait
会一直阻塞主协程直到全部计数都减为0,此时主协程才会被唤醒
我们来看一个简单示例:
package main import ( "fmt" "sync" ) func main() { var wait sync.WaitGroup wait.Add(1) go func() { defer wait.Done() fmt.Println(1) }() wait.Wait() fmt.Println(2) }
输出结果为:
1 2
我们还可以来看一下接下来的这个样例:
package main import ( "fmt" "sync" ) func main() { var mainwait sync.WaitGroup var wait sync.WaitGroup mainwait.Add(10) for i := 0; i < 10; i++ { defer wait.Done() defer mainwait.Done() func(i int) { fmt.Println(i) }(i) wait.Wait() //等循环执行完毕 } mainwait.Wait() }
执行结果为:
0 1 2 3 4 5 6 7 8 9
WaitGroup通常会用于动态调整协程的数量,比如我们在事先就已经知道了协程的数量
又或者是我们在运行过程中需要去动态的调整,而我们在使用Waitgroup
时也不应该
复制它的值,如果我们想将其作为函数参数进行传递的时候,需要传递指针而不是复制它的值
值。倘若使用复制的值,计数完全无法作用到真正的WaitGroup上,这可能会导致主协程一直
阻塞等待,程序将无法正常运行。
示例:
package main import ( "fmt" "sync" ) func main() { var wait sync.WaitGroup wait.Add(1) hello(&wait) wait.Wait() fmt.Println("end") } func hello(wait *sync.WaitGroup) { fmt.Println("hello") wait.Done() }
当计数变为负数,或者计数数量大于子协程数量时,将会引发panic.