自学内容网 自学内容网

GO 语言协程知识点学习笔记

GO 语言协程知识点学习笔记 是个人从互联网上学习整理的笔记。因个人技艺不精,如有纰漏,还请斧正。

协程?

协程并不是 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 读取数据,箭头不指向管道,即代表从管道接收
发送与接收默认是阻塞的

发送于接收默认是阻塞的,这是什么意思呢?当把数据发送至管道时,程序控制会产生阻塞,直到有另外一个协程(可以是主协程)来进行接收,直到其它协程接收了来自管道的数据,反之亦然。否则程序才能继续运行,否则将一直阻塞。

利用该特性,我们可以很容易实现一个不用加锁的同步的协程通信的管道。那么我们如果实现一个发送与接受不阻塞的管道呢?其实只需要将管道增加缓冲即可。


原文地址:https://blog.csdn.net/weixin_45582252/article/details/142882172

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!