「兔了个兔」模拟兔子的一天:浅聊Go协程
我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛
模拟兔子的一天:浅聊Go协程
前言
第一次尝试这种写作风格,希望大家喜欢,不足之处请多多指教
不想看故事部分的小伙伴可以点击文章目录进行跳转
一则小故事
在新年的前一天,一只小兔子决定在家里为大家准备一些新春美食。他想着,他可以利用计算机来帮助他完成菜谱的制作。

于是,小兔子打开了他的电脑,然后使用他最喜欢的编程语言——Golang
。他开始写程序,希望能够在短时间内完成任务。
在编写程序的过程中,小兔子发现他需要同时处理多个任务。于是,他决定使用进程
来帮助他完成这些任务。
进程是指在计算机中执行的一个程序,它可以独立运行并且拥有自己的内存空间。小兔子使用了多个进程来完成任务,这使得他的程序能够高效地运行。
然而,小兔子发现有些任务需要经常在进程之间切换,这使得程序的执行效率降低了。于是,他决定使用线程
来帮助他提高程序的执行效率。
线程是指在进程内执行的一个执行流,它可以与其他线程并发执行。小兔子使用了多个线程来完成任务,这使得他的程序能够更快地运行。
但是,小兔子发现这样做并不太理想,因为线程之间的切换需要系统进行上下文切换,会消耗大量的时间和资源。于是,他决定使用协程
来帮助他更高效地完成任务。
协程是一种轻量级的线程,它可以让程序在执行过程中自动挂起和恢复。这意味着,协程可以在不切换线程的情况下进行切换,这使得它比线程更加轻量和高效。
最终,小兔子成功地使用了进程、线程和协程
来完成了他的任务。他的程序运行得非常顺利,并且在新春的早晨,他准备了一大堆的新春美食,大家都吃得很开心。
正文
看完上面的故事,相信你对协程已经有了一定了解,接下来我们来聊聊Go的协程
定义
在解释协程之前,我们先来看看什么是进程和线程
进程定义
进程是操作系统中执行任务的基本单位,它是一组运行在一个中央处理器上的指令,它可以通过操作系统分配内存和资源来完成各种任务。
进程可以创建、控制和终止其他进程,它还可以通过进程间通信机制与其他进程进行交互。进程是操作系统中的一个重要概念,用于管理和调度系统资源,以确保各种应用程序能够正常运行。
线程定义
线程是操作系统的最小调度单位,它是进程的一个执行流。 一个进程可以包含多个线程,同时多个线程也可以共享进程的资源。
线程是比协程更底层的概念,它需要操作系统来创建和调度。 线程也比协程更加复杂,因为它需要手动管理线程的生命周期,包括创建、启动、挂起和终止等。
协程定义
在计算机系统中,通常使用多线程来解决并发问题。线程是操作系统内核提供的一种执行单元,可以被调度执行,并且拥有自己的内存空间。
然而,传统的线程有一些问题需要解决。首先,线程的创建和销毁需要消耗大量的系统资源,特别是内存。其次,线程之间的切换需要操作系统进行上下文切换,会消耗大量的时间和资源。
为了解决这些问题,就出现了协程这种机制。
协程是一种轻量级的线程,可以在单个进程中同时执行多个任务。 和普通的线程不同,协程不需要操作系统来创建和调度,因此它比线程更轻巧。
协程的工作原理是,在同一个进程中,多个协程之间共享资源,但是它们之间互不影响。 协程可以在不同的时间点挂起和恢复执行,这样就可以让多个协程之间合作完成任务。
协程的工作原理流程图:
[Tips] 在上面的例子中,协程 A 已经从协程 B 获得了一些数据,但是还需要更多的数据来完成任务,所以它再次向协程 B 发送请求,要求协程 B 返回额外的数据。
协程的优点在于它可以有效地利用计算机的多核处理能力,从而提高并行性。 同时,协程还比线程更容易使用,因为它们不需要手动管理线程的生命周期。
此外,协程还具有一些其他优点。 例如,协程可以在不同的协程之间轻松地传递数据,使用通道即可实现。 另外,协程也可以方便地进行错误处理,因为它们可以通过 Go 的内置机制进行层层传递。
线程、进程、协程区别
线程、进程、协程是计算机中三种常见的资源分配单位。 它们都是用来执行任务的,但是它们在资源分配、执行方式、系统开销等方面有所不同。
线程是进程的一个执行单元,进程是计算机中执行任务的基本单位,而协程是一种轻量级的线程。
三者的关系可以用下面的流程图表示:
三者的区别:
线程 | 进程 | 协程 | |
---|---|---|---|
资源分配 | 在进程内共享资源 | 在进程间独立分配资源 | 在单个进程内共享资源 |
执行方式 | 并发执行 | 抢占性执行 | 并发执行 |
系统开销 | 较小 | 较大 | 较小 |
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协程的一些细节和示例代码。
创作不易,如果你觉得本文对你有帮助,可以点赞、评论、收藏,你的支持是我最大的动力,下次更新会更快!



最后,祝大家兔年快乐!
转载自:https://juejin.cn/post/7186165432477286459