我正在参与「兔了个兔」构思投稿大赛,概况请看:「兔了个兔」构思投稿大赛
模仿兔子的一天:浅聊Go协程
前语
第一次测验这种写作风格,期望咱们喜爱,不足之处请多多指教
不想看故事部分的小伙伴能够点击文章目录进行跳转
一则小故事
(故事的代码完成见文末码上)
在新年的前一天,一只小兔子决议在家里为咱们预备一些新春美食。他想着,他能够运用核算机来协助他完结菜谱的制作。
于是,小兔子打开了他的电脑,然后运用他最喜爱的编程言语——Golang
。他开始写程序,期望能够在短时刻内完结使命。
在编写程序的过程中,小兔子发现他需求一起处理多个使命。于是,他决议运用进程
来协助他完结这些使命。
进程是指在核算机中履行的一个程序,它能够独立运转而且拥有自己的内存空间。小兔子运用了多个进程来完结使命,这使得他的程序能够高效地运转。
可是,小兔子发现有些使命需求经常在进程之间切换,这使得程序的履行功率降低了。于是,他决议运用线程
来协助他进步程序的履行功率。
线程是指在进程内履行的一个履行流,它能够与其他线程并发履行。小兔子运用了多个线程来完结使命,这使得他的程序能够更快地运转。
可是,小兔子发现这样做并不太抱负,因为线程之间的切换需求体系进行上下文切换,会耗费大量的时刻和资源。于是,他决议运用协程
来协助他更高效地完结使命。
协程是一种轻量级的线程,它能够让程序在履行过程中主动挂起和康复。这意味着,协程能够在不切换线程的情况下进行切换,这使得它比线程更加轻量和高效。
终究,小兔子成功地运用了进程、线程和协程
来完结了他的使命。他的程序运转得十分顺利,而且在新春的早晨,他预备了一大堆的新春美食,咱们都吃得很高兴。
正文
看完上面的故事,相信你对协程现已有了一定了解,接下来咱们来聊聊Go的协程
界说
在解释协程之前,咱们先来看看什么是进程和线程
进程界说
进程是操作体系中履行使命的基本单位,它是一组运转在一个中央处理器上的指令,它能够经过操作体系分配内存和资源来完结各种使命。
进程能够创立、控制和停止其他进程,它还能够经过进程间通信机制与其他进程进行交互。进程是操作体系中的一个重要概念,用于办理和调度体系资源,以确保各种应用程序能够正常运转。
sequenceDiagram
操作体系 ->> 进程 A: 创立进程
进程 A ->> 进程 B: 进程间通信
操作体系 ->> 进程 A: 调度进程
操作体系 ->> 进程 A: 履行进程
进程 A ->> 操作体系: 完结使命
操作体系 ->> 进程 A: 停止进程
线程界说
线程是操作体系的最小调度单位,它是进程的一个履行流。 一个进程能够包含多个线程,一起多个线程也能够同享进程的资源。
线程是比协程更底层的概念,它需求操作体系来创立和调度。 线程也比协程更加复杂,因为它需求手动办理线程的生命周期,包括创立、发动、挂起和停止等。
sequenceDiagram
主线程 ->> 线程 A: 创立线程
线程 A ->> 主线程: 获取数据
主线程 -->> 线程 A: 回来数据
线程 A ->> 线程 B: 线程间通信
线程 B -->> 线程 A: 回来成果
线程 A ->> 主线程: 回来成果
主线程 ->> 线程 A: 停止线程
协程界说
在核算机体系中,一般运用多线程来解决并发问题。线程是操作体系内核提供的一种履行单元,能够被调度履行,而且拥有自己的内存空间。
可是,传统的线程有一些问题需求解决。首先,线程的创立和毁掉需求耗费大量的体系资源,特别是内存。其次,线程之间的切换需求操作体系进行上下文切换,会耗费大量的时刻和资源。
为了解决这些问题,就呈现了协程这种机制。
协程是一种轻量级的线程,能够在单个进程中一起履行多个使命。 和一般的线程不同,协程不需求操作体系来创立和调度,因而它比线程更轻巧。
协程的工作原理是,在同一个进程中,多个协程之间同享资源,可是它们之间互不影响。 协程能够在不同的时刻点挂起和康复履行,这样就能够让多个协程之间合作完结使命。
协程的工作原理流程图:
sequenceDiagram
协程 A ->> 协程 B: 恳求数据
协程 B -->> 协程 A: 回来数据
协程 A ->> 协程 B: 恳求额定的数据
协程 B -->> 协程 A: 回来额定的数据
协程 A ->> 协程 B: 恳求终究数据
协程 B -->> 协程 A: 回来终究数据
协程 A ->> 协程 B: 完结使命
协程 B -->> 协程 A: 使命完结
[Tips] 在上面的例子中,协程 A 现已从协程 B 获得了一些数据,可是还需求更多的数据来完结使命,所以它再次向协程 B 发送恳求,要求协程 B 回来额定的数据。
协程的长处在于它能够有效地运用核算机的多核处理能力,然后进步并行性。 一起,协程还比线程更容易运用,因为它们不需求手动办理线程的生命周期。
此外,协程还具有一些其他长处。 例如,协程能够在不同的协程之间轻松地传递数据,运用通道即可完成。 另外,协程也能够方便地进行错误处理,因为它们能够经过 Go 的内置机制进行层层传递。
线程、进程、协程区别
线程、进程、协程是核算机中三种常见的资源分配单位。 它们都是用来履行使命的,可是它们在资源分配、履行方法、体系开支等方面有所不同。
线程是进程的一个履行单元,进程是核算机中履行使命的基本单位,而协程是一种轻量级的线程。
三者的联系能够用下面的流程图表明:
graph LR
thread[线程]
process[进程]
coroutine[协程]
thread --> process
process --> coroutine
classDef resource fill:#f9f,stroke:#333,stroke-width:4px;
classDef execution fill:#99f,stroke:#333,stroke-width:4px;
class process resource
class thread execution
class coroutine execution
三者的区别:
线程 | 进程 | 协程 | |
---|---|---|---|
资源分配 | 在进程内同享资源 | 在进程间独立分配资源 | 在单个进程内同享资源 |
履行方法 | 并发履行 | 抢占性履行 | 并发履行 |
体系开支 | 较小 | 较大 | 较小 |
Go协程
在上面的故事中,小白兔运用协程一起处理多个使命,而这也是协程所要解决的问题——并发问题。 Go言语中的协程(又称为”goroutine”)是由Go言语内部完成的轻量级线程。它们的完成与传统的线程有很大的不同,有一些特别之处。
不同于其他编程言语,Go运用通信通道(channel)来传递数据。这使得只有一个协程(goroutine)能够访问数据,防止了竞态条件的呈现。
一言以蔽之:不是经过同享内存通信,而是经过通信同享内存
(这句话详见:Go 博客中关于经过通信同享内容的帖子)
Go协程的调度是由Go言语运转时(runtime)主动办理的,而不是由操作体系内核调度的。这意味着,Go协程能够在不切换内核线程的情况下切换,然后防止了线程切换带来的开支。
其次,Go协程是协作式多使命,而不是抢占式多使命。这意味着,在Go言语中,协程之间是经过让出时刻片的方法来协作的,而不是经过抢占处理器的方法。这使得Go协程在多个协程之间切换时,更加高效。
最终,Go协程是十分轻量级的。在Go言语中,创立一个新的协程只需求2kb左右的的内存空间,而传统的线程一般需求几兆的内存空间。这使得Go协程能够创立不计其数个,而不会对体系带来太大的负担。
可是,Go协程也有一些缺点,因为Go协程是由Go言语运转时主动调度的,因而它们之间并没有固定的履行联系,也就是说,不能保证某个协程在另一个协程之前履行。因而,在运用Go协程时,需求留意防止呈现竞态条件的情况。
此外,Go协程也不支持传统的线程同步机制,如互斥锁、信号量等。因而,在运用Go协程时,需求留意运用适当的同步方法,例如channel、sync.WaitGroup等。
[Tips] 例子:
这是一个核算斐波拉契数列的程序,但因为没有运用同步机制,所以呈现了竞态条件。
package main
import (
"fmt"
"sync"
)
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Print(fib(n), ",")
}(i)
}
wg.Wait()
}
输出:
解决方案:
package main
import "fmt"
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
示例代码
为了更好地展现go中如何运用协程,这里我给出一些示例代码
发动协程:
package main
import "fmt"
func main() {
// 发动一个协程
go func() {
fmt.Println("Hello, World!")
}()
}
运用通道
在协程中传输数据:
package main
import "fmt"
func main() {
// 创立一个整型通道
ch := make(chan int)
// 发动一个协程
go func() {
// 向通道中写入数据
ch <- 1
}()
// 从通道中读取数据
fmt.Println(<-ch)
}
运用通道
进行同步:
package main
import "fmt"
func main() {
// 创立一个整型通道
ch := make(chan int)
// 发动一个协程
go func() {
// 在协程中履行一些核算
result := 1 + 1
// 将核算成果写入通道
ch <- result
}()
// 从通道中读取核算成果
result := <-ch
fmt.Println(result)
}
运用 select
分支多个通道的数据:
package main
import "fmt"
func main() {
// 创立两个整型通道
ch1 := make(chan int)
ch2 := make(chan int)
// 发动两个协程
go func() {
ch1 <- 1
}()
go func() {
ch2 <- 2
}()
// 运用 select 分支多个通道的数据
select {
case result := <-ch1:
fmt.Println(result)
case result := <-ch2:
fmt.Println(result)
}
}
运用 context
包办理协程的生命周期:
package main
import (
"context"
"fmt"
)
func main() {
// 创立一个带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// 发动一个协程
go func() {
// 运用 select 分支上下文中的 done 通道
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
}
}()
// 等待协程完毕
<-ctx.Done()
}
总结
本文从一个小兔子预备新春美食的故事讲起,讲了进程、线程、协程的界说和三者的区别,并讲解了Go协程的一些细节和示例代码。
创造不易,假如你觉得本文对你有协助,能够点赞、评论、收藏,你的支持是我最大的动力,下次更新会更快!
最终,祝咱们兔年快乐!
码上