美文网首页谈技术读书摄影
Go 并发可视化解释 — 通道

Go 并发可视化解释 — 通道

作者: 技术的游戏 | 来源:发表于2023-08-28 00:15 被阅读0次

当谈到并发时,许多编程语言采用共享内存/状态模型。然而,Go 通过实现通信顺序进程 (CSP) 来区别自己。在CSP中,一个程序由并行进程组成,这些进程不共享状态;相反,它们使用通道来通信并同步它们的行为。

因此,对于有兴趣采用 Go 的开发者来说,理解通道的工作原理变得至关重要。在这篇文章中,我将使用 Gophers 运行他们的想象咖啡店的有趣类比来描述通道,因为我坚信人们更容易通过视觉来学习。

场景

Partier、Candier 和 Stringer 正在经营一个咖啡馆。由于制作咖啡所需的时间比接受订单要长,Partier 会协助接受顾客的订单,然后将这些订单传递给厨房,Candier 和 Stringer 在那里准备咖啡。

无缓冲通道

最初,咖啡店以最简单的方式运作:每当收到一个新订单时,Partier 将订单放入通道,并等待 Candier 或 Stringer 拿走它,然后再接受任何新订单。通过无缓冲通道,使用 ch := make(chan Order) 创建,实现了 Partier 和厨房之间的这种沟通。当通道中没有挂起的订单时,即使 Stringer 和 Candier 都准备好接受新订单,他们仍然处于等待状态,等待新订单到来。

当收到一个新订单时,Partier 将其放入通道,使该订单可以被 Candier 或 Stringer 拿走。但在接受新订单之前,Partier 必须等待其中一个人从通道中取出订单。

当 Stringer 和 Candier 都准备好接受新订单时,新订单将立即被其中一个拿走。然而,不能保证或预测到底是谁会拿到订单。Stringer 和 Candier 之间的选择是不确定的,这取决于诸如调度和 Go 运行时的内部机制之类的因素。假设 Candier 得到了这个第一个订单。

在 Candier 完成处理第一个订单后,她回到等待状态。如果没有新的订单到来,两个工人,Candier 和 Stringer,都会闲置,直到 Partier 将另一个订单放入通道供他们处理。

当收到一个新订单,且 Stringer 和 Candier 都可以处理它时。即使 Candier 刚刚处理了上一个订单,接收新订单的特定工人仍然是不确定的。在这个场景中,我们假设 Candier 再次被分配了这第二个订单。

新的订单 order3 到来,由于 Candier 正在处理 order2,她并没有等在 order := <-ch 这一行,Stringer 成为了唯一可以接收 order3 的可用工人。因此,他会得到它。

order3 发送给 Stringer 之后,order4 立即到达。此时,Stringer 和 Candier 都已经在处理他们各自的订单,没有人可以拿走 order4。因为通道没有缓冲,将 order4 放入通道会阻塞 Partier,直到 Stringer 或 Candier 有一个变得可以接受 order4。我经常看到人们对无缓冲通道(用 make(chan order)make(chan order, 0) 创建)和缓冲大小为1的通道(用 make(chan order, 1) 创建)感到困惑。因此,他们错误地期望 ch <- order4 立即完成,允许 Partier 接受 order5,然后在 ch <- order5 上被阻塞。如果你也有这种想法,我在 Go Playground 上创建了一个代码片段,帮助你纠正误解 https://go.dev/play/p/shRNiDDJYB4

缓冲通道

无缓冲通道确实可以工作,但它限制了整体的吞吐量。如果他们仅接受一定数量的订单在后台(厨房)顺序处理会更好。这可以通过缓冲通道来实现。现在,即使 Stringer 和 Candier 忙于处理他们的订单,只要通道没有满,Partier 仍然可以在通道中留下新的订单并继续接受其他订单,例如,最多3个挂起的订单。

通过引入缓冲通道,咖啡店增强了处理更多订单的能力。但是,选择适当的缓冲大小以保持合理的客户等待时间是至关重要的。毕竟,没有客户愿意忍受过长的等待时间。有时,拒绝新订单可能比接受它们并且无法及时完成它们更为可取。此外,在短暂的容器化(Docker)应用程序中使用缓冲通道时必须小心,因为预期会有随机重启,在这种情况下,从通道中恢复消息可能是一项具有挑战性的任务,甚至可能是不可能的。

通道与阻塞队列

尽管它们在本质上是不同的,但Java中的Blocking Queue是用来在线程之间通信的,而Go中的Channel是用于Goroutine的通信,BlockingQueue 和 Channel 的行为在某种程度上是相似的。如果你熟悉BlockingQueue,那么理解Channel肯定会很容易。

常见用途

通道是Go应用中的一个基础且广泛使用的功能,服务于各种目的。通道的一些常见用途包括:

  • Goroutine 通信:通道使不同的goroutines之间能够进行消息交换,使它们可以协作而无需直接共享状态。
  • 工作池:正如上面的示例中所见,通道经常用于管理工作池,其中多个相同的工作人员从共享通道处理传入的任务。
  • 扇出,扇入:通道促进了扇出,扇入模式,其中多个goroutines(扇出)执行工作并将结果发送到一个通道,而另一个goroutine(扇入)消费这些结果。
  • 超时和截止日期:与 select 语句结合使用,通道可以用于处理超时和截止日期,确保程序可以优雅地处理延迟并避免无限的等待。

我将在其他文章中更详细地探讨通道的不同用途。但是,现在,让我们通过实现上述的咖啡店场景来结束这篇入门博客,并亲眼看到通道如何在实践中工作。我们将探索Partier、Candier和Stringer之间的互动,并观察通道如何促进他们之间的顺畅沟通和协调,使咖啡店能够有效地处理订单和同步。

给我看你的代码!

package main

import (
    "fmt"
    "log"
    "math/rand"
    "sync"
    "time"
)

func main() {
    ch := make(chan order, 3)

    wg := &sync.WaitGroup{} // More on WaitGroup another day
    wg.Add(2)

    go func() {
        defer wg.Done()
        worker("Candier", ch)
    }()

    go func() {
        defer wg.Done()
        worker("Stringer", ch)
    }()

    for i := 0; i < 10; i++ {
        waitForOrders()
        o := order(i)
        log.Printf("Partier: I %v, I will pass it to the channel\n", o)
        ch <- o
    }

    log.Println("No more orders, closing the channel to signify workers to stop")
    close(ch)

    log.Println("Wait for workers to gracefully stop")
    wg.Wait()

    log.Println("All done")
}

func waitForOrders() {
    processingTime := time.Duration(rand.Intn(2)) * time.Second
    time.Sleep(processingTime)
}

func worker(name string, ch <-chan order) {
    for o := range ch {
        log.Printf("%s: I got %v, I will process it\n", name, o)
        processOrder(o)
        log.Printf("%s: I completed %v, I'm ready to take a new order\n", name, o)
    }
    log.Printf("%s: I'm done\n", name)
}

func processOrder(_ order) {
    processingTime := time.Duration(2+rand.Intn(2)) * time.Second
    time.Sleep(processingTime)
}

type order int

func (o order) String() string {
    return fmt.Sprintf("order-%02d", o)
}

您可以复制这段代码,在您的IDE上进行调整并运行它,以更好地理解通道是如何工作的。

如果您对保持对软件工程领域的最新动态感兴趣,请关注我。我将确保让您了解最新信息!

相关文章

  • 通道

    通道是 Go 并发编程中重要的一员 基础知识 通道是 Go 自带的,唯一一个可以满足并发安全性的类型。 声明并初始...

  • <>

    通道的基本操作# 通道本身是并发安全的,这也是go语言自带的唯一一个可以满足并发安全的的类型。 通道的初始化 通道...

  • 16 Go并发编程(三):死锁 —— Go并发编程的陷阱

    协程死锁 学完Go的协程与通道,我们已经对Go的并发编程有大概的了解,可以说go的并发程序还是很容易编写的,只要深...

  • Go控制程序生命周期

    近期学了go语言的并发编程,掌握了 并发原语-原子锁,互斥锁,通道 并发原理 并发设计模式今天我们就来锻炼一下,如...

  • 你知道Python有一个并发编程库吗?goless

    前言 使用goless库,您可以用Python的Go语言风格编写并发程序。goess提供了通道、select和go...

  • Go 并发编程:通道常见应用范式

    通道经典应用 一、闭包实现通道访问限制 在Go的并发编程中,创建通道和开辟协程是非常方便且容易的,正因如此,有可能...

  • go之通道类型

    1.基本知识 通道类型的值本身就是并发安全的,go语言自带的唯一一个可以满足并发安全的类型。通道相当于一个先进先出...

  • Golang 学习笔记十 并发编程 锁

    参考《快学 Go 语言》第 13 课 —— 并发与安全 上一节我们提到并发编程不同的协程共享数据的方式除了通道之外...

  • Golang 通道,同步等待组 并发爬虫

    Golang:通道,同步等待组 并发爬虫 在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通...

  • go channel详解之源码分析

    作为golang并发编程思想的重要组成,channel(通道)非常重要,和goroutine(go协程)一起使用,...

网友评论

    本文标题:Go 并发可视化解释 — 通道

    本文链接:https://www.haomeiwen.com/subject/iefgmdtx.html