我正在参与「兔了个兔」构思投稿大赛,概况请看:「兔了个兔」构思投稿大赛

模仿兔子的一天:浅聊Go协程

前语

第一次测验这种写作风格,期望咱们喜爱,不足之处请多多指教

不想看故事部分的小伙伴能够点击文章目录进行跳转

一则小故事

(故事的代码完成见文末码上)

在新年的前一天,一只小兔子决议在家里为咱们预备一些新春美食。他想着,他能够运用核算机来协助他完结菜谱的制作。

「兔了个兔」模拟兔子的一天:浅聊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()
}

输出:

「兔了个兔」模拟兔子的一天:浅聊Go协程

解决方案:

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协程的一些细节和示例代码。

创造不易,假如你觉得本文对你有协助,能够点赞、评论、收藏,你的支持是我最大的动力,下次更新会更快!

「兔了个兔」模拟兔子的一天:浅聊Go协程
「兔了个兔」模拟兔子的一天:浅聊Go协程
「兔了个兔」模拟兔子的一天:浅聊Go协程

最终,祝咱们兔年快乐!

码上 「兔了个兔」模拟兔子的一天:浅聊Go协程