go 启动协程
对于如下代码
package main
import "fmt"
func main() {
go foo()
go bar()
fmt.Println("end")
}
func foo() {
for i := 0; i < 45; i++ {
fmt.Println("Foo:", i)
}
}
func bar() {
for i := 0; i < 45; i++ {
fmt.Println("Bar:", i)
}
}
如果我不加上 print end 的话,居然是什么都不会输出的,即便加上了,也只会输出
Foo: 0
Foo: 1
Foo: 2
end
说明如果主进程结束了就直接没有了,可能还是需要join的用法
这段代码之所以可以跑,是因为主进程也执行了循环,变相的等待了一段时间,如果没有这个的话,也是跑不了的
刚才上面的结论是错误的,能跑不是因为执行了循环,循环是很快的,能跑的根本原因是函数内部执行了sleep 拖慢了时间导致的,不然只会执行到主函数的
这种情况下,使用管道可以完成同步,chan 是用来管理并发和同步的一种常用方法,例如
package main
import "fmt"
func main() {
done := make(chan bool)
go gosomething(done)
<-done
}
func gosomething(done chan bool) {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
done <- true
}
注意管道的声明是有2个 chan bool 两个单词组成的,所以在形参的时候,要给出三个,参数名 chan 类型
另外我发现go 关于管道的定义 ch <- v // 把 v 发送到通道 ch v := <-ch // 从 ch 接收数据 并把值赋给 v
你说为什么不定义为 v -> ch 表示发送到通道 ch 呢 v := <- ch 这个倒是没有什么问题,或者 v := ch-> 其实也可以的
问,ai 所最主要是是和C的结构体访问的 -> 保持一致了
单个的可以使用管道,但是像上面,同样的函数调用,得看看怎么使用管道了
package main
import "fmt"
func main() {
done := make(chan bool)
go say("hello", done)
<-done
say("world", done)
}
func say(s string, done chan bool) {
for i := 0; i < 10; i++ {
//fmt.Println(i + " " + s)
fmt.Println(s)
}
done <- true
}
这样子又会报错 fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]: main.say({0x10a72d7, 0x5}, 0x1094a00?)
一种修改的方法,是把第二次的,也放到协程中运行,也就是连续两次
go say("hello", done)
<-done
go say("world", done)
<-done
查询之后,如果一定是一次在协程中运行一次在主函数中运行,那么ai 第一次给出的还是使用 sync.WatiGroup的用法,如果强制要使用 chan的话,可以让第二个chan 变成可选参数,或者变长参数,这样子就不需要一定要传递 chan 了
其实如果这样子的话,我再多加个参数,判断到底是否要调用chan 发送也是可以的
func main() {
done := make(chan bool)
go say("hello", done, 1)
<-done
say("world", done, 2)
}
func say(s string, done chan bool, cnt int) {
for i := 0; i < 10; i++ {
//fmt.Println(i + " " + s)
fmt.Println(s)
}
if cnt == 1 {
done <- true
}
}
如果使用可选参数的话,那么就是
func main() {
done := make(chan bool)
go say("hello", done)
<-done
say("world")
close(done)
}
func say(s string, chs ...chan bool) {
for i := 0; i < 10; i++ {
//fmt.Println(i + " " + s)
fmt.Println(s)
}
for _, done := range chs {
done <- true
}
}
这里需要注意的是,传递的 chan 变成了一个 ... 变成了一个数组 这是因为,go 里面没有可选参数的概念,所以可选参数是通过变长参数来完成的,如果什么都不穿的话,那么就相当于None了 变长参数,是通过在 参数类型前面加上...来表示一个变长参数,所以在这里,变长参数,就变成传递一个数组了,所以才需要一个循环来判断,不能直接 done <- true 了
所以可选参数,就是通过这个变长来实现的,除了变长参数实现之外,还可以通过结构体和函数来实现
模拟结构体 和 函数作为参数的话,大概就是使用者这边去判断是否要取了
另外还有一个点就是,在上面的代码中,显示声明了close(chan) ,为什么之前没有显示声明,现在显示声明呢 ?
写和不写,区别不大,close 只是一个显示的表述,如果没有写的话,那么main运行完成后,就会离开作用域,并且它没有赋予任何其他用途,所以go 运行时会自动清理,显示关闭chan是一个好习惯,尤其在多个goroutine环境中,有助于清晰地表达代码意图
使用sync.WatiGroup
使用 wg 的话就是这样的代码
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
//go say3("Hello")
//go say3("World")
go func() {
say3("Hello")
defer wg.Done()
}()
go func() {
say3("World")
defer wg.Done()
}()
wg.Wait()
}
func say3(s string) {
for i := 0; i < 10; i++ {
fmt.Println(s)
}
}
那么问题来了,为什么defer 要在函数执行之前,而不是执行之后呢 这是一个习惯性的写法,把 defer 写在之前的话,就能确保不管在say3中发生了什么(如某些原因需要提前返回),wg.Done()都会被调用,这是go并发编程中的一种常见模式 ,用于确保 wg.WaitGroup 计数器再 goroutine 完成其工作后能正确递减
初始听这句话的时候,其实没太明白是为什么,什么叫做 say3 发生什么情况,多年执行到defer, 用人话来说,是这样的
func someFunction() {
resource := acquireResource() // 获取资源
defer releaseResource(resource) // 无论何种退出,都会释放资源 // ... }
.... 然后才是业务逻辑...
你如果把defer 写在后面的话,万一你一进来,就崩溃报错了,就panic了,那么后面要执行的defer 就没法执行了
所以defer 实际上是要求放在所有东西的开头,不管你后面业务逻辑怎么糟糕,怎么搞崩溃这个函数,都能去执行defer
另外把defer写在最前面还有下面这几种好处
- 因为在业务逻辑之前,确保业务逻辑即使崩溃的情况下,也能执行到defer的部分,不管任何时候,defer的部分都能被执行到
- 避免遗漏:写在最前面,放在函数体开头的部分减少了遗漏defer的可能
- 可读性:提高了代码的可读性,其他的开发者可以快速的看到,哪些资源或操作会在函数退出的时候被清理掉
go run -race 参数
加上-race 之后能够进行竞争检测,在go中,由于其并发特性,不同的goroutine同时运行,如果这些goroutine没有适当的同步机制,他们可能会同时尝试修改同一个变量,从而造成不可预期的结果
go run -race 通过引用一种特殊的运行时监测机制来检测是否会出现这种情况,如果出现了这种情况,那么程序将会被终止,比如下面这段代码
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
var counter int
func main() {
wg.Add(2)
go incrementor("Foo:")
go incrementor("Bar:")
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incrementor(s string) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 20; i++ {
x := counter
x++
time.Sleep(time.Duration(rand.Intn(3)) * time.Millisecond)
counter = x
fmt.Println(s, i, "Counter:", counter)
}
wg.Done()
}
// go run -race main.go
// vs
// go run main.go
同时修改一个count 那么-race就会是下面这种情况
==================
WARNING: DATA RACE
Write at 0x00000121dc10 by goroutine 6:
main.incrementor()
GolangTraining/22_go-routines/06_race-condition/main.go:27 +0x145
main.main.func1()
GolangTraining/22_go-routines/06_race-condition/main.go:15 +0x37
Previous read at 0x00000121dc10 by goroutine 7:
main.incrementor()
GolangTraining/22_go-routines/06_race-condition/main.go:24 +0x104
main.main.func2()
GolangTraining/22_go-routines/06_race-condition/main.go:16 +0x37
Goroutine 6 (running) created at:
main.main()
GolangTraining/22_go-routines/06_race-condition/main.go:15 +0x44
Goroutine 7 (running) created at:
main.main()
GolangTraining/22_go-routines/06_race-condition/main.go:16 +0x50
==================
Bar: 0 Counter: 1
Bar: 1 Counter: 2
Foo: 0 Counter: 1
Foo: 1 Counter: 3
Bar: 2 Counter: 3
Bar: 3 Counter: 4
Bar: 4 Counter: 5
Bar: 5 Counter: 6
Bar: 6 Counter: 7
Foo: 2 Counter: 4
Bar: 7 Counter: 8
Foo: 3 Counter: 5
Bar: 8 Counter: 9
Foo: 4 Counter: 6
Bar: 9 Counter: 10
Bar: 10 Counter: 11
Bar: 11 Counter: 12
Bar: 12 Counter: 13
Foo: 5 Counter: 7
Bar: 13 Counter: 14
Foo: 6 Counter: 14
Bar: 14 Counter: 15
Bar: 15 Counter: 16
Bar: 16 Counter: 17
Foo: 7 Counter: 15
Foo: 8 Counter: 16
Foo: 9 Counter: 17
Bar: 17 Counter: 18
Foo: 10 Counter: 18
Foo: 11 Counter: 19
Foo: 12 Counter: 20
Foo: 13 Counter: 21
Foo: 14 Counter: 22
Bar: 18 Counter: 19
Bar: 19 Counter: 20
Foo: 15 Counter: 23
Foo: 16 Counter: 24
Foo: 17 Counter: 25
Foo: 18 Counter: 26
Foo: 19 Counter: 27
Final Counter: 27
Found 1 data race(s)
exit status 66
能看到详细的检测数据
另外这里他是把 wg 设置成一个全局变量,所以在子函数里面也能使用
所以如果wg 不是全局变量的话,就可以写成这种形式
go func() {
defer wg.Done()
incrementor("Foo:")
}()
go func() {
defer wg.Done()
incrementor("Bar:")
}()
注意func(){} 后面一定要加上()启动
要解决刚才的这个同步问题的话,那么就需要加锁,也就是
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
var counter int
var mutex sync.Mutex
func main0() {
wg.Add(2)
go incrementor("Foo:")
go incrementor("Bar:")
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incrementor(s string) {
for i := 0; i < 20; i++ {
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond)
mutex.Lock()
counter++
fmt.Println(s, i, "Counter:", counter)
mutex.Unlock()
}
wg.Done()
}
但是要注意一点:
mutex.Lock()
counter++
fmt.Println(s, i, "Counter:", counter)
mutex.Unlock()
就是这个print 也要放到加锁的里面,不然 -race 的时候也会报错,会有一个Previous read 这个警告,混淆读也会有告警,这个就非常不错
go 协程相关的问题-可以无限创建吗
比如无限创建协程的代码
package main;
import "fmt"
import "math"
import "runtime"
func main() {
taskCount := math.MaxInt64
for i:=0; i<taskCount; i++ {
go func(i int){
fmt.Printf("go func ", i, " goroutine count: ", runtime.NumGoroutine() , "\n")
// fmt.Printf("go func %d, goroutine count: %d\n", i,runtime.NumGoroutine())
// fmt.Println("go func ", i, " goroutine count: ", runtime.NumGoroutine() , "\n")
}(i)
}
}
这个代码直接跑的话,会把电脑卡主,但是用任务管理器去看的话,线程实际上还是只有11个,是创建的协程多
这个代码第一眼看到的时候,其实就有问题:这真的能创建吗,即便是go了协程,应该是马上就被销毁了,因为就执行完毕了,或者说等到主线程结束之后,也就销毁了,实测也是这样的
不过在这个之前,还有一个要处理,就是 fmt.Printf 不能使用Println 这样用逗号连接的操作,既然Printf是要格式化的的话,只能按照格式化的模式去写,也就是:
被注释掉的 fmt.Printf("go func %d, goroutine count: %d\n", i,runtime.NumGoroutine()) 这种的写法
执行下来的表现,就是执行到一定程度之后,会被卡主,比如
然后是长时间的卡主,不断的卡主,翻到最后看起来只循环到了751,其实不是这样的,往上翻,其实
早就循环到了2000多,只不过输出早了,到751 卡主一段时间之后,然后再才是结束掉
类似于这样的效果,我甚至有点怀疑,是等到主循环全部结束之后,才kill 掉退出的,这个我们用defer 试试看, 看看defer 能不能有效果,打印出来当最后退出的时候,还有多少个协程,但是改成
func main() {
defer func(){
fmt.Printf("Exit, goroutine count: %d\n", runtime.NumGoroutine())
}()
taskCount := math.MaxInt64
for i:=0; i<taskCount; i++ {
go func(i int){
fmt.Printf("go func %d, goroutine count: %d\n", i,runtime.NumGoroutine())
}(i)
}
}
后,明明有defer 但是还是没有效果,那么降低数量看看,defer 是应该要执行的,当改成10以后
确实defer 的效果就出来了,实际上可以看出,当时运行了6个协程,但是最后只输出了一个,应该是只有一个协程到了输出的阶段,其他还没到输出的阶段,就随着主进程的结束而结束掉了(但是实际上协程已经生成了,只是还没来得及运行) 这也是我想提到的点,第一眼看这代码的时候,我就想到,主进程结束了,那就都结束了,之所以这个测试代码有效,就是因为主进程循环的事件很长,所以导致可以看到效果 现在对这个代码改造一下,使用管道,让他可以持续运行
把代码改造一下,又不想传递太多参数,于是打算把管道写成全局变量的形式,但是如果写成
// done := make(chan []int, taskCount)
// taskCount := math.MaxInt64
// taskCount = 10
var taskCount int64
taskCount = 10
done := make([]int , taskCount)
这样一直会报错,之前以为是管道定义方式的问题,于是干脆测试make 一个数组,结果数组也是不行,以为是不能:= 于是写成变量定义的形式,结果还是不行,当时就有点蒙。。回看之前的代码,才领悟到,对于全局变量来说,在全局范围内,是只能定义,不能赋值或者初始化
我想这样定义 var taskCount int64 然后赋值taskCount = int64(10) 为什么不能强制转化类型呢
答案就是,如果将一个值复制给已经声明了类型的变量的时候,如果该值的类型,与变量的类型相同,那么go 将进行隐式的转化(前提是能兼容,比如 int int64 这种就是兼容的)
另外其实这个强制转换也是个错的,因为for循环的时候给的是 i:=0 给的是int类型,而不是int64 就算要强制转换的话,也应该是把int64转化成int类型才对,修改之后,我想这么写,居然也还是不行
package main;
import "fmt"
import "math"
import "runtime"
var taskCount int64
var done chan []bool
func main() {
defer func(){
fmt.Printf("Exit, goroutine count: %d\n", runtime.NumGoroutine())
}()
taskCount = math.MaxInt64
taskCount = 10
done = make(chan []bool, taskCount)
for i:=0; int64(i)<taskCount; i++ {
go func(i int){
defer func(){
done <- true
}()
fmt.Printf("go func %d, goroutine count: %d\n", i,runtime.NumGoroutine())
}(i)
}
}
报错说,done 是一个数组,所以不能写入true, 但是如果不是数组的话,我写入那么多个,是真的可以的吗
先不管管道的用法,还是可以用wg waitgroup来实现,用wg的话,一下子就写好了
可以看出,执行完了以后,居然会有11个协程,同时到最后退出的时候,就只剩下一个,那么如果我在退出之前,多等待一下呢,会不会这些协程,全部都没有了
不对,应该是在defer的时候sleep
这个协程应该是没有必要,因为wait的话,能打印出来这一个,应该一定是所有协程都执行完了,才会打印
我的目的是希望看到协程的数量打印出来不一样,应该是在defer 里面去sleep 指定的秒杀才对
defer func(){
wg.Done()
time.Sleep( i * 1000 * time.Millisecond )
}()
直接写sleep还不对了,invalid operation: i * 1000 * time.Millisecond (mismatched types int and time.Duration) 改成 time.Sleep( time.Duration(i * 1000 * time.Millisecond) ) 也还是同样的报错,因为MillilSecond 是64位的,我改成
time.Sleep( time.Duration( int64(i) * int64(1000) * time.Millisecond) )
也是不行,应该是要用Duration转化医学
time.Sleep( time.Duration( time.Duration(i) * 1000 * time.Millisecond) )
这样就可以了
但是实测好像没有效果,始终还是11个协程,那么回到开始的问题,就是因为用管道阻塞,没有起作用,才用wg WaitGroup的,现在要探究,到底应该怎么用管道了
如果改成一个bool的管道的话
虽然是能运行,但是也是不对的,因为么有执行完全,如果打开了 <- done的话,那么就会报错了
day3 $ go run manygo.go
go func 0, goroutine count: 11
go func 1, goroutine count: 9
go func 5, goroutine count: 11
go func 9, goroutine count: 11
go func 7, goroutine count: 11
go func 4, goroutine count: 11
go func 2, goroutine count: 11
go func 8, goroutine count: 11
go func 3, goroutine count: 11
go func 6, goroutine count: 11
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:
main.main()
21daygo/day3/manygo.go:36 +0x98
goroutine 17 [chan send (nil chan)]:
main.main.func2.1()
21daygo/day3/manygo.go:31 +0x39
main.main.func2(0x0)
21daygo/day3/manygo.go:34 +0x108
created by main.main
21daygo/day3/manygo.go:29 +0x6e
goroutine 18 [chan send (nil chan)]:
main.main.func2.1()
21daygo/day3/manygo.go:31 +0x39
main.main.func2(0x1)
21daygo/day3/manygo.go:34 +0x108
created by main.main
21daygo/day3/manygo.go:29 +0x6e
goroutine 19 [chan send (nil chan)]:
main.main.func2.1()
21daygo/day3/manygo.go:31 +0x39
main.main.func2(0x2)
21daygo/day3/manygo.go:34 +0x108
created by main.main
21daygo/day3/manygo.go:29 +0x6e
正确的做法,要发送的信号,应该是一个chan的切片,而不是单个的值,同时在最后,也是要循环取的,切片就是
done <- chan []bool{true}
不对,只需要发送一个bool切片就行了,不需要声明切片,也就是
done <- bool[] {true}
所以完整的代码就是
package main;
import "fmt"
import "math"
import "runtime"
var taskCount int64
var done chan []bool
func main() {
defer func(){
fmt.Printf("Exit, goroutine count: %d\n", runtime.NumGoroutine())
}()
taskCount = math.MaxInt64
taskCount = 10
done = make(chan []bool, taskCount)
for i:=0; int64(i)<taskCount; i++ {
go func(i int){
defer func(){
done <- [] bool {true}
}()
fmt.Printf("go func %d, goroutine count: %d\n", i,runtime.NumGoroutine())
}(i)
}
for i:=0; int64(i) < taskCount; i++ {
<-done
}
}
其实不用写成数组的形式的,也就是说声明chan的时候,不需要声明成切片的形式,按照原先的代码,直接发送 done<-true 就是可以的,就是在主函数接收的时候,变成循环就可以
另外不需要吧chan 声明为全局变量居然也是可以的
这是因为启动的goroutine 是main内的匿名函数,我独立出去就不行了
但是奇怪的是,我独立出去以后,虽然idea没报错,但是运行的时候报错了
找到问题了,是变量声明的问题
重复定义了
另外还有一点可以提到的,就是对于chan 可以设置为有缓冲和无缓冲的 有缓冲的定义就是
done = make(chan bool, size)
无缓冲的就是
done = make(chan bool)
如果要使用无缓冲的话,那么就要确定,每一个发送,一定会有一个接收,要一一对应起来,这通常用于同步,因为同步的情况下,发送和接收都是相对应的
在这些用法中,其实还是 wg 用法是最好的,是处理协程同步,最常见和最清晰的方式,特别是当你不需要协程的其他功能(比如通信)的时候
如果你是需要通信,那才用信号去同步