协程
- 协程:在Go语言中关键字为
go
。一般有耗时函数,想要优化成并行运行函数就可以用。 - 协程是一起执行的,除非手动设置否则不分先后并发执行。
- 使用协程启用函数,对于该协程函数和
main
函数是并发执行的,main
函数运行在一个特殊的协程上(主协程)。 - 主线程结束之后,协程函数也会结束。
- 启动一个新协程时会立即返回,程序控制不会等待协程执行完成,这就意味着调用协程函数是不会执行协程函数的任何逻辑。
- 调用协程后会立即返回到主函数代码接着向下运行,忽略该协程的任何返回值。
- 如果主协程终止,则程序终止,其他协程也就终止。所以为了让其他协程运行逻辑,我们可以让主协程后执行,也就是调用
time.sleep
函数,但是这样休眠主函数的时间是否能让新协程运行完逻辑是不确定的,这就关系到了通道的问题。 - 所以不推荐协程间使用休眠的方式来进行互相的等待。
现在通过下面的代码来验证上述大部分的说法:
package main import ( "fmt" "time" ) func learning(name string) { fmt.Printf("%s 开始学习\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 结束学习\n", name) } func main() { startTime := time.Now() //下面这样子操作就像是接力,一个人去回来以后下一个人才能去 //learning("张三") //learning("李四") //learning("王五") //使用协程就可以让他们一起去一起回了 go learning("张三") go learning("李四") go learning("王五") //但是用了上面这个协程做法之后,会发现输出直接变成了main函数里的输出,结束了整个程序 //原因是因为使用了协程之后,调用的learning函数就不是和main函数在一个线程了,这样main函数只用执行协程之外的命令 //所以造成主协程结束之后,分支协程也结束,直接输出主协程结果后结束的现象 //为此我们暂时解决上面那个问题就需要将main函数延时 //time.Sleep(1 * time.Second) //但是会发现延时一秒只完成了部分 //所以这个方法只是土鳖方法,经不起推敲,延迟短 了运行不完整,延时长了造成浪费。 fmt.Println("某人学习结束", time.Since(startTime)) }
- 所以可以用一个非常简陋的方法解决这些问题:
package main import ( "fmt" "time" ) var wait int func learning(name string) { fmt.Printf("%s 开始学习\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 结束学习\n", name) wait-- } func main() { startTime := time.Now() wait = 3 go learning("张三") go learning("李四") go learning("王五") for { if wait == 0 { break } } fmt.Println("某人学习结束", time.Since(startTime)) } //我们加一个wait值用来当信号量,当wait归零时即协程完成,就可以接着执行主函数的内容。
- Go官方也想到了这个问题,所以给了一个
sync.WaitGroup
类型用来确认协程的数量。我们可以用这个来让这段代码完善起来: WaitGroup
的方法:Add() 初始值是0,累加子协程的数量 Done() 当某个子协程完成后,计数器-1,通常用defer调用 Wait() 阻塞当前协程,直到实例的计数器归零
package main import ( "fmt" "sync" "time" ) //wait不是全局变量,所以也要将函数参数进行改变 //wait用指针的原因就和前面指针那一章讲的一样,主函数的wait与函数参数的wait是不一样的,函数参数的wait是拷贝过来的,所以要加指针去指向主函数的原wait。 func learning(name string, wait *sync.WaitGroup) { fmt.Printf("%s 开始学习\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 结束学习\n", name) wait.Done() //告知协程完成 } func main() { startTime := time.Now() var wait sync.WaitGroup //这样wait可以不用当全局变量,避免了许多隐性问题 wait.Add(3) //事先添加协程的总数量有多少个 go learning("张三", &wait) go learning("李四", &wait) go learning("王五", &wait) wait.Wait() //等待添加的携程都结束 fmt.Println("某人学习结束", time.Since(startTime)) }
- 其实定义
WaitGroup
用全局变量也可以,但是程序复杂起来还是得在主函数内声明方便,而且全局变量本身也会引发一些其他问题。 - 所以最终还是推荐能在主函数内声明就在主函数内声明。
- 要启动多协程则要搞懂新协程与主协程之间的关系。
- 还有就是如果要从协程内拿取东西出来进行操作,也和通道(
channel
)有关。
通道(channel)
/*
通道的声明 //chan为channel的缩写
var channel_name chan channel_type
*/
- 通道:Go语言中协程之间通信的管道就叫通道(信道、channel),在Go语言中,通道的关键字为
chan
。 如果运行报错:
fatal error: all goroutines are asleep - deadlock!
说明协程被死锁了。举个例子:package main import ( "fmt" "sync" "time" ) //var book chan string //只能存对应的单一类型,和map差不多,一开始创建内部的值就是nil // 想要初始化通道可以用make函数,声明并初始化一个长度为0的通道 var bookChan = make(chan string) //通道也是有长度的 // 长度为0的通道就相当于人搬运一个货物只能抓着腾不出手,等人拿了才能接着拿别的货。长度为1的通道类推,人可以放一个货之后再抓住一个货,取的人可以两个货一起取也可以二选一。 func learning(name string, book string, wait *sync.WaitGroup) { fmt.Printf("%s 开始学习\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 结束学习\n", name) bookChan <- book //<-为通道赋值标识符 wait.Done() } func main() { startTime := time.Now() var wait sync.WaitGroup wait.Add(3) go learning("张三", "大学高数", &wait) go learning("李四", "大学英语", &wait) go learning("王五", "大学物理", &wait) wait.Wait() fmt.Println("某人学习结束", time.Since(startTime)) } //这时候写的不完善程序直接运行会报错,原因出在于通道的长度以及没有命令去通道取东西出来导致死锁。
- 所以优化一下:
package main import ( "fmt" "sync" "time" ) //var book chan string //只能存对应的单一类型,和map差不多,一开始创建内部的值就是nil // 想要初始化通道可以用make函数,声明并初始化一个长度为0的通道 var bookChan = make(chan string) //通道也是有长度的 // 长度为0的通道就相当于人搬运一个货物只能抓着腾不出手,等人拿了才能接着拿别的货。长度为1的通道类推,人可以放一个货之后再抓住一个货,取的人可以两个货一起取也可以二选一。 func learning(name string, book string, wait *sync.WaitGroup) { fmt.Printf("%s 开始学习\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 结束学习\n", name) bookChan <- book //<-为通道赋值标识符 wait.Done() } func main() { startTime := time.Now() var wait sync.WaitGroup wait.Add(3) go learning("张三", "大学高数", &wait) go learning("李四", "大学英语", &wait) go learning("王五", "大学物理", &wait) for { //这里的死循环充当阻塞作用 bookName, ok := <-bookChan //这样这里还是会出问题,因为是死循环,当通道内的东西被拿完后还会接着拿,就会接着触发死锁。 fmt.Println(bookName, ok) if !ok { fmt.Println("协程内值拿不了") break } } wait.Wait() fmt.Println("某人学习结束", time.Since(startTime)) }
- 所以要解决上面代码的问题,就要在合适的时机将通道关闭:
package main import ( "fmt" "sync" "time" ) var bookChan = make(chan string) func learning(name string, book string, wait *sync.WaitGroup) { fmt.Printf("%s 开始学习\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 结束学习\n", name) bookChan <- book //<-为通道赋值标识符 wait.Done() } func main() { startTime := time.Now() var wait sync.WaitGroup wait.Add(3) go learning("张三", "大学高数", &wait) go learning("李四", "大学英语", &wait) go learning("王五", "大学物理", &wait) //新建一个协程函数 go func() { defer close(bookChan) //写法② wait.Wait() //close(bookChan) //写法① }() //for { // bookName, ok := <-bookChan // fmt.Println(bookName, ok) // if !ok { // fmt.Println("协程内值拿不了") // break // } //} //简便写法 var bookList []string for bookName := range bookChan { bookList = append(bookList, bookName) } wait.Wait() fmt.Println("某人学习结束", time.Since(startTime)) fmt.Println("刚刚的人都学了", bookList) }
然后细心的可能会发现,协程的输出顺序是无序的,每次可能都会不一样,这是因为协程是一起并行执行的,更何况这三个协程调用的是同一个协程函数,运行完成时间几乎一致,所以导致的输出没有顺序。
评论(0)