21天速成go-第三天

94 阅读15分钟

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的用法

image.png

这段代码之所以可以跑,是因为主进程也执行了循环,变相的等待了一段时间,如果没有这个的话,也是跑不了的

image.png

刚才上面的结论是错误的,能跑不是因为执行了循环,循环是很快的,能跑的根本原因是函数内部执行了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的结构体访问的 -> 保持一致了

image.png

单个的可以使用管道,但是像上面,同样的函数调用,得看看怎么使用管道了

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 了

所以可选参数,就是通过这个变长来实现的,除了变长参数实现之外,还可以通过结构体和函数来实现

image.png

模拟结构体 和 函数作为参数的话,大概就是使用者这边去判断是否要取了

另外还有一个点就是,在上面的代码中,显示声明了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()) 这种的写法

执行下来的表现,就是执行到一定程度之后,会被卡主,比如

image.png

然后是长时间的卡主,不断的卡主,翻到最后看起来只循环到了751,其实不是这样的,往上翻,其实

image.png

早就循环到了2000多,只不过输出早了,到751 卡主一段时间之后,然后再才是结束掉

image.png

类似于这样的效果,我甚至有点怀疑,是等到主循环全部结束之后,才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以后

image.png

确实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的话,一下子就写好了

image.png

可以看出,执行完了以后,居然会有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) )

这样就可以了

image.png

但是实测好像没有效果,始终还是11个协程,那么回到开始的问题,就是因为用管道阻塞,没有起作用,才用wg WaitGroup的,现在要探究,到底应该怎么用管道了

如果改成一个bool的管道的话

image.png

虽然是能运行,但是也是不对的,因为么有执行完全,如果打开了 <- 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 就是可以的,就是在主函数接收的时候,变成循环就可以

image.png

另外不需要吧chan 声明为全局变量居然也是可以的

image.png

这是因为启动的goroutine 是main内的匿名函数,我独立出去就不行了

image.png

但是奇怪的是,我独立出去以后,虽然idea没报错,但是运行的时候报错了

image.png

找到问题了,是变量声明的问题

image.png

重复定义了

另外还有一点可以提到的,就是对于chan 可以设置为有缓冲和无缓冲的 有缓冲的定义就是

done = make(chan bool, size)

无缓冲的就是

done = make(chan bool)

如果要使用无缓冲的话,那么就要确定,每一个发送,一定会有一个接收,要一一对应起来,这通常用于同步,因为同步的情况下,发送和接收都是相对应的

在这些用法中,其实还是 wg 用法是最好的,是处理协程同步,最常见和最清晰的方式,特别是当你不需要协程的其他功能(比如通信)的时候

如果你是需要通信,那才用信号去同步