首页 > 后端开发 > Golang > 通过直观的视觉效果了解 Golang 中的 Goroutines 和 Channel

通过直观的视觉效果了解 Golang 中的 Goroutines 和 Channel

Mary-Kate Olsen
发布: 2024-12-30 16:18:09
原创
782 人浏览过

⚠️ 这个系列如何进行?

1。运行每个示例:不要只阅读代码。输入它,运行它,然后观察其行为。
2。实验和打破常规: 删除睡眠并看看会发生什么,更改通道缓冲区大小,修改 goroutine 计数。
打破东西会教你它们是如何工作的
3。关于行为的原因: 在运行修改后的代码之前,尝试预测结果。当您看到意外行为时,请停下来思考原因。挑战解释。
4。建立心理模型:每个可视化代表一个概念。尝试为修改后的代码绘制自己的图表。

Understanding Goroutines and Channels in Golang with Intuitive Visuals

这是“掌握 Go 并发”系列的 第 1 部分,我们将介绍:

  • goroutine 的工作原理及其生命周期
  • goroutines 之间的通道通信
  • 缓冲通道及其用例
  • 实际示例和可视化

我们将从基础知识开始,逐步发展如何有效使用它们的直觉。

这会有点长,相当长,所以做好准备。

Understanding Goroutines and Channels in Golang with Intuitive Visuals

我们将亲力亲为完成整个过程。

Goroutine 的基础

让我们从一个下载多个文件的简单程序开始。

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

Understanding Goroutines and Channels in Golang with Intuitive Visuals

该程序总共需要 6 秒,因为每个 2 秒的下载必须在下一个开始之前完成。让我们想象一下:

Understanding Goroutines and Channels in Golang with Intuitive Visuals

我们可以缩短这个时间,让我们修改我们的程序以使用go例程

注意:函数调用前使用 go 关键字

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    // Launch downloads concurrently
    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    fmt.Println("All downloads completed!")
}
登录后复制
登录后复制
登录后复制

等等什么?没有打印任何内容?为什么?

Understanding Goroutines and Channels in Golang with Intuitive Visuals

让我们想象一下这一点,以了解可能发生的情况。

Understanding Goroutines and Channels in Golang with Intuitive Visuals

从上面的可视化中,我们了解到 main 函数在 goroutine 完成之前就存在了。一项观察结果是,所有 goroutine 的生命周期都依赖于 main 函数。

注意:main函数本身就是一个goroutine;)

为了解决这个问题,我们需要一种方法让主 goroutine 等待其他 goroutine 完成。有几种方法可以做到这一点:

  1. 等待几秒钟(黑客方式)
  2. 使用 WaitGroup(正确的方法,下一步)
  3. 使用频道(我们将在下面介绍)

让我们等待几秒钟让 go 例程完成。

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

问题是,我们可能不知道 goroutine 可能需要多长时间。在这种情况下,我们每个人都有固定的时间,但在实际场景中,我们知道下载时间会有所不同。

来了sync.WaitGroup

Go中的sync.WaitGroup是一种并发控制机制,用于等待一组goroutines执行完成。

在这里让我们看看它的实际效果并可视化:

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    // Launch downloads concurrently
    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    fmt.Println("All downloads completed!")
}
登录后复制
登录后复制
登录后复制

Understanding Goroutines and Channels in Golang with Intuitive Visuals

让我们可视化这一点并了解sync.WaitGroup 的工作原理:

Understanding Goroutines and Channels in Golang with Intuitive Visuals

计数器机制:

  • WaitGroup 维护一个内部计数器
  • wg.Add(n) 将计数器增加 n
  • wg.Done() 将计数器减 1
  • wg.Wait() 阻塞,直到计数器达到 0

同步流程:

  • 主 Goroutine 在启动 Goroutines 之前调用 Add(3)
  • 每个 goroutine 完成时都会调用 Done()
  • 主协程在 Wait() 处被阻塞,直到计数器达到 0
  • 当计数器达到 0 时,程序继续并干净地退出

要避免的常见陷阱
package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now() // Record start time

    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    // Wait for goroutines to finish
    time.Sleep(3 * time.Second)

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
登录后复制
登录后复制

渠道

这样我们就很好地理解了 goroutine 是如何工作的。不,两个 Go 例程如何通信?这就是频道发挥作用的地方。

Go 中的

Channels 是一个强大的并发原语,用于 goroutine 之间的通信。它们为 goroutine 提供了一种安全共享数据的方法。

将通道视为管道:一个 goroutine 可以将数据发送到通道,另一个 goroutine 可以接收数据。

以下是一些属性:

  1. 通道本质上是阻塞的。
  2. A 发送到通道操作 ch 阻塞直到其他 Goroutine 从通道接收。
  3. 从通道接收操作阻塞直到其他goroutine发送到通道。
package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

Understanding Goroutines and Channels in Golang with Intuitive Visuals

为什么 ch

Understanding Goroutines and Channels in Golang with Intuitive Visuals

让我们通过添加 goroutine 来解决这个问题

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    // Launch downloads concurrently
    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    fmt.Println("All downloads completed!")
}
登录后复制
登录后复制
登录后复制

让我们想象一下:

Understanding Goroutines and Channels in Golang with Intuitive Visuals

这次消息从不同的 Goroutine 发送,因此主 Goroutine 在发送到通道时不会被阻塞,因此它会移动到 msg :=

使用通道修复 main 不等待其他问题

现在让我们使用channel来修复文件下载器问题(main不等待其他人完成)。

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now() // Record start time

    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    // Wait for goroutines to finish
    time.Sleep(3 * time.Second)

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
登录后复制
登录后复制

Understanding Goroutines and Channels in Golang with Intuitive Visuals

可视化它:

Understanding Goroutines and Channels in Golang with Intuitive Visuals

让我们进行一次演练以更好地理解:

节目开始

主协程创建完成通道
启动三个下载 goroutine
每个 goroutine 都会获得对同一通道的引用

下载执行:

  1. 所有三个下载同时运行
  2. 每个需要2秒
  3. 他们可能会以任何顺序完成

频道循环:

  1. 主协程进入循环: for i := 0;我
  2. 每个
  3. 循环确保我们等待所有三个完成信号

Understanding Goroutines and Channels in Golang with Intuitive Visuals

循环行为:

  1. 迭代 1:阻塞直至首次下载完成
  2. 迭代 2:阻塞直到第二次下载完成
  3. 迭代 3:阻塞直至最终下载完成

完成顺序并不重要!

观察
⭐ 每次发送(完成 ⭐ 主协程通过循环协调一切

两个goroutine如何通信?

我们已经了解了两个 goroutine 如何进行通信。什么时候?一直以来。 我们不要忘记 main 函数也是一个 goroutine

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

让我们想象一下并试运行一下:

Understanding Goroutines and Channels in Golang with Intuitive Visuals

试运行:

程序开始(t=0ms)

  • 主协程初始化三个通道:
    • ch:用于消息。
    • senderDone:表示发送者完成。
    • receiveDone:表示接收器完成。
  • 主协程启动两个协程:
    • 发件人。
    • 接收器。
  • 主 Goroutine 块,等待来自

第一条消息(t=1ms)

  1. 发送者将“消息1”发送到ch通道。
  2. 接收者醒来并处理消息:
    • 打印:“已收到:消息 1”
  3. 发送者休眠 100 毫秒。

第二条消息(t=101ms)

  1. 发送者醒来并向ch通道发送“消息2”。
  2. 接收方处理消息:
    • 打印:“已收到:消息 2”
  3. 发送者再休眠 100 毫秒。

第三条消息(t=201ms)

  1. 发送者醒来并向ch通道发送“消息3”。
  2. 接收方处理消息:
    • 打印:“已收到:消息 3”
  3. 发送者最后一次睡觉。

通道关闭(t=301ms)

  1. 发送者完成睡眠并关闭 ch 通道。
  2. 发送者向 senderDone 通道发送一个 true 信号以指示完成。
  3. 接收器检测到ch通道已关闭。
  4. 接收器退出其 for-range 循环。

完成(t=302-303ms)

  1. 主协程收到来自senderDone的信号并停止等待。
  2. 主协程开始等待来自 receiveDone 的信号。
  3. 接收器向receiverDone通道发送完成信号。
  4. 主协程接收信号并打印:
    • “所有操作完成!”.
  5. 程序退出。

缓冲通道

为什么我们需要缓冲通道?
无缓冲的通道会阻塞发送方和接收方,直到另一方准备好为止。当需要高频通信时,无缓冲的通道可能会成为瓶颈,因为两个 goroutine 必须暂停来交换数据。

缓冲通道属性:

  1. FIFO(先进先出,类似于队列)
  2. 固定大小,在创建时设置
  3. 缓冲区已满时阻止发送者
  4. 当缓冲区为空时阻塞接收器

Understanding Goroutines and Channels in Golang with Intuitive Visuals

我们看到它的实际效果:

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

输出(在取消注释 ch

Understanding Goroutines and Channels in Golang with Intuitive Visuals

为什么它没有阻塞主协程?

  1. 缓冲通道允许发送至其容量而不阻塞发送者。

  2. 通道的容量为 2,这意味着它在阻塞之前可以在缓冲区中保存两个值。

  3. 缓冲区已经满了“第一”和“第二”。由于没有并发接收者来使用这些值,因此发送操作会无限期地阻塞。

  4. 因为主 goroutine 也负责发送,并且没有其他活动的 goroutine 从通道接收值,所以程序在尝试发送第三条消息时陷入死锁。

取消注释第三条消息会导致死锁,因为容量现在已满,第三条消息将阻塞,直到缓冲区释放。

Understanding Goroutines and Channels in Golang with Intuitive Visuals

何时使用缓冲通道与非缓冲通道

Aspect Buffered Channels Unbuffered Channels
Purpose For decoupling sender and receiver timing. For immediate synchronization between sender and receiver.
When to Use - When the sender can proceed without waiting for receiver. - When sender and receiver must synchronize directly.
- When buffering improves performance or throughput. - When you want to enforce message-handling immediately.
Blocking Behavior Blocks only when buffer is full. Sender blocks until receiver is ready, and vice versa.
Performance Can improve performance by reducing synchronization. May introduce latency due to synchronization.
Example Use Cases - Logging with rate-limited processing. - Simple signaling between goroutines.
- Batch processing where messages are queued temporarily. - Hand-off of data without delay or buffering.
Complexity Requires careful buffer size tuning to avoid overflows. Simpler to use; no tuning needed.
Overhead Higher memory usage due to the buffer. Lower memory usage; no buffer involved.
Concurrency Pattern Asynchronous communication between sender and receiver. Synchronous communication; tight coupling.
Error-Prone Scenarios Deadlocks if buffer size is mismanaged. Deadlocks if no goroutine is ready to receive or send.

要点

使用 缓冲 通道如果:

  1. 你需要解耦发送者和接收者的时间。
  2. 批处理或排队消息可以提高性能。
  3. 当缓冲区已满时,应用程序可以容忍处理消息的延迟。

使用 无缓冲 通道,如果:

  1. goroutines 之间的同步至关重要。
  2. 您想要简单并立即传递数据。
  3. 发送者和接收者之间的交互必须即时发生。

这些基础知识为更高级的概念奠定了基础。在我们即将发布的帖子中,我们将探讨:

下一篇文章:

  1. 并发模式
  2. 互斥和内存同步

请继续关注我们,我们将继续加深对 Go 强大并发功能的理解!

Understanding Goroutines and Channels in Golang with Intuitive Visuals

以上是通过直观的视觉效果了解 Golang 中的 Goroutines 和 Channel的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板