likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 68

我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

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

前言

第一次尝试这种写作风格,希望大家喜欢,不足之处请多多指教

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

一则小故事

在新年的前一天,一只小兔子决定在家里为大家准备一些新春美食。他想着,他可以利用计算机来帮助他完成菜谱的制作。

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

于是,小兔子打开了他的电脑,然后使用他最喜欢的编程语言——Golang。他开始写程序,希望能够在短时间内完成任务。

在编写程序的过程中,小兔子发现他需要同时处理多个任务。于是,他决定使用进程来帮助他完成这些任务。

进程是指在计算机中执行的一个程序,它可以独立运行并且拥有自己的内存空间。小兔子使用了多个进程来完成任务,这使得他的程序能够高效地运行。

然而,小兔子发现有些任务需要经常在进程之间切换,这使得程序的执行效率降低了。于是,他决定使用线程来帮助他提高程序的执行效率。

线程是指在进程内执行的一个执行流,它可以与其他线程并发执行。小兔子使用了多个线程来完成任务,这使得他的程序能够更快地运行。

但是,小兔子发现这样做并不太理想,因为线程之间的切换需要系统进行上下文切换,会消耗大量的时间和资源。于是,他决定使用协程来帮助他更高效地完成任务。

协程是一种轻量级的线程,它可以让程序在执行过程中自动挂起和恢复。这意味着,协程可以在不切换线程的情况下进行切换,这使得它比线程更加轻量和高效。

最终,小兔子成功地使用了进程、线程和协程来完成了他的任务。他的程序运行得非常顺利,并且在新春的早晨,他准备了一大堆的新春美食,大家都吃得很开心。

正文

看完上面的故事,相信你对协程已经有了一定了解,接下来我们来聊聊Go的协程

定义

在解释协程之前,我们先来看看什么是进程和线程

进程定义

进程是操作系统中执行任务的基本单位,它是一组运行在一个中央处理器上的指令,它可以通过操作系统分配内存和资源来完成各种任务。

进程可以创建、控制和终止其他进程,它还可以通过进程间通信机制与其他进程进行交互。进程是操作系统中的一个重要概念,用于管理和调度系统资源,以确保各种应用程序能够正常运行。

操作系统进程 A进程 B创建进程进程间通信调度进程执行进程完成任务终止进程操作系统进程 A进程 B

线程定义

线程是操作系统的最小调度单位,它是进程的一个执行流。 一个进程可以包含多个线程,同时多个线程也可以共享进程的资源。

线程是比协程更底层的概念,它需要操作系统来创建和调度。 线程也比协程更加复杂,因为它需要手动管理线程的生命周期,包括创建、启动、挂起和终止等。

主线程线程 A线程 B创建线程获取数据返回数据线程间通信返回结果返回结果终止线程主线程线程 A线程 B

协程定义

在计算机系统中,通常使用多线程来解决并发问题。线程是操作系统内核提供的一种执行单元,可以被调度执行,并且拥有自己的内存空间。

然而,传统的线程有一些问题需要解决。首先,线程的创建和销毁需要消耗大量的系统资源,特别是内存。其次,线程之间的切换需要操作系统进行上下文切换,会消耗大量的时间和资源。

为了解决这些问题,就出现了协程这种机制。

协程是一种轻量级的线程,可以在单个进程中同时执行多个任务。 和普通的线程不同,协程不需要操作系统来创建和调度,因此它比线程更轻巧。

协程的工作原理是,在同一个进程中,多个协程之间共享资源,但是它们之间互不影响。 协程可以在不同的时间点挂起和恢复执行,这样就可以让多个协程之间合作完成任务。

协程的工作原理流程图:

协程 A协程 B请求数据返回数据请求额外的数据返回额外的数据请求最终数据返回最终数据完成任务任务完成协程 A协程 B

[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()
}

输出:

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

最后,祝大家兔年快乐!

转载自:https://juejin.cn/post/7186165432477286459
评论
请登录