一、Goroutines
如果之前有学习过操作系统,或者其他语言,例如JAVA,那么对这个其实是不陌生了,与 Thread 类似,也就是新创建并执行了一个线程,这个线程可以跟当前的主线程或者其他线程一起执行,从而大大提高并发度,提高对CPU的使用
主要使用关键字:go
我们以一个斐波那契数列为例子,在这个例子当中,使用递归的算法计算它,但是因为数字大,并且计算效率低下,我们需要在计算的过程中,创建一个新的线程,告诉用户用户当前计算正在执行中,我们就可以使用 go 来实现,如下:
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
二、channel
(一) 什么是 channel ?
继Goroutines出现之后,就有一个问题,如果一个主线程当中,有多个Goroutines,并且这些Goroutines之间还要有一定的先后关系,例如GoroutinesA执行完之后,再执行GoroutinesB
用人话来说,GoroutinesA执行完毕之后,告诉GoroutinesB,好,你可以执行了!
如果需求更多一些,GoroutinesB可能需要GoroutinesA将一些数据,传给GoroutinesB,作为参数去运行
这其实就是Goroutines之间的通信问题,说道这里你也就明白了,channel 是做什么的了
channel: 作为一个管道,将不同的Goroutines建立起来信息通信,确定线程的执行先后顺序
(二) channel 的声明
一般来说,我们都通过make 函数创建 一个无缓存的channel,那么一定还有有缓存的,之后说,五缓存的 channel 创建方式如下:
ch := make(chan int) // ch has type 'chan int'
有两个参数,第一个为 chan ,标明是管道类型,第二个是对应管道接收的参数,这里的 int 就代表对应的管道可以用来传输 int 类型的数据
参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
此外,两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
此外,一个channel主要有发送和接收数据这两个主要操作,全都是用来进行通信的,发送时,使用 : <- [需要传输的数据] 将对应的数据传输到对应类型得 channel 当中
接收时,使用: <- [对应包含数据的 channel]
举一个例子:
ch := make(chan int)
ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded
如果对应的channel没有传输数据,但是存在一个channel去接收数据,那么此时就会阻塞线程,直到这个 channel 接收到了数据,并被消费数据,否则就会一直被阻塞。
但是有一点需要注意!看代码:
func main(){
ch := make(chan int)
ch <- 10
fmt.Println("代码执行完毕")
}
虽然编译没有问题,但是一运行,这里就会在第三行出现问题,出现:fatal error: all goroutines are asleep - deadlock! 的错误
这是因为对于无缓存的channel来说,就相当于是一个快递员送东西的,但是没有快递站存储快递,为什么有快递员?就是因为有用户买东西了,才有了!也就是说,对于无缓存的channel来说,必须要有其他的消费数据,才能有数据存储,这一点需要记住
简单的说,如果一个channel 没有被关闭,那么对应消费数据时,就会一直等着数据的到来,哪怕实际上已经没数据了!
这个时候就会有聪明人问了:那么怎么保证一些数据被线程接收和消费完毕了,当前的主线程就不再接收数据,选择结束,也就是不再阻塞了呢?
哎!我们可以通过自主的关闭一个channel来做到
关闭的时候,之后再次对这个 channel 发起任何的调用都会出现 panic 异常,如下:
close(ch)
同时,关闭了之后,对应的接收数据接收完之后,就不会再进行阻塞式的等待了
(三) Goroutines 的线程同步
我们需要先理解一个概念,在并发编程中,我们说线程A先于线程B执行或者发生,意思不是说线程A执行的比B早,而是说要保证,B执行的时候,A已经执行完毕了,之后就可以放心的依赖这个工作结果去完成自己的任务了,如下:
ch := make(chan struct{})
go func() {
log.Println("goroutine1正在执行,等待5S执行完毕")
time.Sleep(5 * time.Second)
log.Println("goroutine1已经执行完毕!")
ch <- struct{}{}
}()
<-ch
fmt.Println("主线程已经关闭")
代码中,我们的一个线程正在执行一个任务,逻辑我们暂时定位睡5S
其实在主线程中,我们的运行结果早已经到了 <- ch 这一步,但是一直都在被阻塞,因为还没接收到对应 chnnel 发送的数据,所以一直在阻塞
从而做到了Goroutines当中的执行,一定发生在 main 结束之前
这里我们channel 类型为 struct{},其实没有什么意义,就是代表需要进行线程同步,标识一下,没有实际内容或者数据,也可以用int 或者 bool,代表对应线程是否执行完毕
三、串联的 channels (Pipeline)
一个 channel 可以接收Goroutines的数据并且传输,那么要是一个流程需要多个线程之间相关传输信息,怎么办?就像一个流水线一样,需要多个搬运工去传输信息
这个时候我们只需要多添加几个 channel ,形成一个Pipeline,就行啦
例如 GoroutinesA -> channelA -> GoroutinesB -> channelB -> Println
这里就是两个线程信息的传递,我们使用了两个channel 去保证信息的传递,这就是 pipeline
举一个例子,如下:
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; ; x++ {
naturals <- x
}
}()
// Squarer
go func() {
for {
x := <-naturals
squares <- x * x
}
}()
// Printer (in main goroutine)
for {
fmt.Println(<-squares)
}
}
但是有一个问题,对应需要传输的数据应该都是有限的,要是这样,最后的 for 不就一直接收一直大一i你,那就是一个永远不停的了,这可不对,我们对Counter 修改,例如只需要传输10个数据
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0;x < 10 ; x++ {
naturals <- x
}
}()
// Squarer
go func() {
for {
x := <-naturals
squares <- x * x
}
}()
// Printer (in main goroutine)
for {
fmt.Println(<-squares)
}
}
但是接收起来,最后一个 for 循环打印信息还是停不下来,怎么办?
有聪明的同学就想起来了,这是因为 channel 没有关闭,导致对应接收一直在等待!
是的,当channel被关闭了,之后接收的时候就不会阻塞,直接返还0值,但是依旧挑不出最后的 for
别怕,其实获取数据的时候,还有一个参数:
x, ok := <-naturals
没错,ok作为第二个参数,代表对应的 channel 是否被关闭!
我们可以通过这个来结束死循环
但是还有更简单的方法,记得 for range 吗?我们直接 range 对应的 channel 即可,这样没有数据接收之后直接结束!更新代码如下:
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}
四、channel buffer
上面我们说过,channel采用make去确定chan的类型,这是一种无缓存的通道创建,但其实后面可以再跟上一个参数,用来确定对应的channel的缓存数量,创建方式如下:
channelA := make(chan int,2)
代表创建了一个缓存大小为2的管道,但是这个缓存有什么用呢?
哎!用处大了,记得之前说的吗?无缓存的通道,每次接收只能接收一个数据,满了之后并且没有人接收,那么就会阻塞,无法接收新的数据
但是有了缓存,我们就可以获取多个数据,那么没有被及时的接收,只要缓存未被占满,就能继续接收,不会阻塞!直到缓存被占满
最好想到的一个作用,就是对一些操作进行限制:
例如,现在我们做了一个文件大小遍历程序,用来查看对应文件夹下所有文件大小的总和,通常采用并发迭代的方法去遍历,但是如果一直让其打开对应的文件,不加以限制,就可能会导致资源被占用过多,甚至造成崩溃!所以我们可以对打开文件的多少进行限制
这个时候我们就可以采用缓存channel了!设置对应buffer大小,被占满了,就不准再打开新的文件夹了,直到有一个被使用