GO 语言协程知识点学习笔记 (1)
GO 语言协程知识点学习笔记 (1) 是个人从互联网上学习整理的笔记。因个人技艺不精,如有纰漏,还请斧正。
协程?
协程并不是 GO 语言特有的机制,像 Lua、Ruby、Python、Kotlin、C/C++ 也都有协程的支持。区别在于有些是从语言层面支持,有的通过插件类库支持。Go 语言的协程是原生语言层面支持的。
协程和进程和线程的对比
进程
- 进程是系统资源分配的最小单位。进程的创建和销毁都是系统资源级别。操作起来代价昂贵。程是抢占式调度,分为三个状态:等待态、就绪态、运行态。进程之间是相互隔离的,拥有各自的系统资源,更加安全。因此存在进程之间通讯不方便的问题
- 进程是线程的载体容器,多个线程除了共享进程的资源还拥有自己的一少部分独立的资源。因此相比进程而言线程更加清凉,进程内的多个线程间的通信比进程容易,但也同样带来了同步和互斥的问题和线程安全的问题(多线程编程的常见问题),尽管多线程编程仍然是当前服务端编程的主流,线程也是 CPU 调度的最小单位,多线程运行时存在线程切换的问题,其状态的转移如图:
协程
- 协程在有的资料中成为微线程或者用户态轻量级线程,协程调度不需要系统内核参与而是完全由用户态程序来决定,因此协程对于系统而言是无感知的。协程由用户态控制就不存在抢占式调度。抢占式调度会强制让 CPU 控制权切换到其它进线程,多个协程进行协作实调度,协程自己主动把控制权转让出去后,其它协程 才能被执行到,这样就避免了系统切换开销提高了 CPU 的使用效率。
抢占式调度和协作式调度的简单对比图:
Go 协程
启用一个 Go 协程非常简单,只需要在函数前加上关键字 go
,就可以轻易的启用一个新的 Go 协程并发运行。
package main
import "fmt"
func main() {
go hello()
fmt.Println("main function")
}
func hello() {
fmt.Println("Hello goroutine")
}
go hello()
此处启动了一个协程,因此hello()
函数会与 main()
函数并发执行。main()
函数会运行在一个特有的 Go 协程上,它被称为 Go 的主协程(Main Goroutine)。
运行结果大概率如下:
main function
Process finished with exit code 0
为什么只输出了 main function
?我们的 Hello goroutine
为什么没有输出呢?
- 当启动一个新的协程时,协程的调用会立即返回,与函数不同,程序控制不回去等待 Go 的协程执行完毕,在本例中即
hello()
函数,因此程序控制会立即返回到代码的下一行,即fmt.Println("main function")
,忽略该协程的任何返回值 - 协程的存在与否取决于
main()
主协程是否存活,如果主协程被销毁(即程序终止了),那么其余用 go 关键字启用的协程,在本例中为hello()
协程也被销毁了。这就导致了主协程已经结束,但没有完全等待hello()
协程的执行完毕,就被销毁了。因此出现了以上情况。
看到这里,你应该知道为什么运行结果是大概率如下,因为主协程和协程的执行顺序是不一致的,在一定概率下,即主协程正好等到了协程的执行完毕,会输出 Hello goroutine
。
让我们来修复下这个问题吧!
package main
import (
"fmt"
"time"
)
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
func hello() {
fmt.Println("Hello goroutine")
}
可以看到,我们利用 time
包中的 Sleep
方法,去休眠主协程,让其等待 1 秒钟,等待 hello()
协程执行完毕。那么最终输出结果如下:
Hello goroutine
main function
Process finished with exit code 0
注意
:在实际业务中,利用协程休眠的方式去控制协程的执行顺序是不可取的,因为这样的预测永远无法 100% 如你所愿。如果需要控制协程的同步,我们会在下面讲到 channel
即管道或信道。
启用多个 Go 协程
为了更好的理解 Go 协程,我们再编写一个程序,启动多个 Go 协程
package main
import (
"fmt"
"time"
)
func main() {
go hello()
go anotherHello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
func hello() {
for i := 0; i <= 3; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Println("Hello goroutine")
}
}
func anotherHello() {
for i := 0; i <= 3; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Println("Hello another goroutine")
}
}
hello()
协程会在第一次输出Hello goroutine
后,休眠该协程 250 毫秒,再次输出Hello goroutine
,直到i > 3
。anotherHello()
协程会在第一次输出Hello another goroutine
后,休眠该协程 400 毫秒,再次输出Hello another goroutine
,直到i > 3
。- 最终主协程会在该两个协程执行完毕后,输出
main function
是不是很简单?Go 语言屏蔽了多线程的复杂实现,只需要一个 go 关键字即可轻而易举的创建一个新的协程。
什么是 Channel 管道:
Channel 是 Go 协程之间通信的通信管道,如同管道中的谁会从一端流到另一端,通过使用管道,就可以实现数据从管道的一个端口发送,在另外一个端口接收。
管道声明(无缓冲管道)
声明管道就像声明一个切片一样简单:
package main
func main() {
c := make(chan int)
// 或者
var c2 chan int
}
chan T
表示该管道只能运送T
类型的数据,在本例中为int
类型数据
通过管道进行发送与接收
a := make(chan int)
getDataFromChannelA := <-a // 读取管道 a 发送的内容
a <- 1 // 向管道 a 发送 1
怎么样?是不是很简单。在一开始可能会觉得管道操作符比较难记,其实我们只需要记住简单的方法即可
a <- 1
向管道 a 发送 1,箭头指向管道,即代表发送<- a
从管道 a 读取数据,箭头不指向管道,即代表从管道接收
发送与接收默认是阻塞的
发送于接收默认是阻塞的,这是什么意思呢?当把数据发送至管道时,程序控制会产生阻塞,直到有另外一个协程(可以是主协程)来进行接收,直到其它协程接收了来自管道的数据,反之亦然。否则程序才能继续运行,否则将一直阻塞。
利用该特性,我们可以很容易实现一个不用加锁的同步的协程通信的管道。那么我们如果实现一个发送与接受不阻塞的管道呢?其实只需要将管道增加缓冲即可。
未完待续。
插眼,好文学习