登录  /  注册
首页 > 后端开发 > Golang > 正文

一文详解Go中的并发【20 张动图演示】

青灯夜游
发布: 2022-09-08 10:48:26
转载
1553人浏览过

golang中各种并发模式看起来是怎样的?下面本篇文章就通过20 张动图为你演示 go 并发,希望对大家有所帮助!

一文详解Go中的并发【20 张动图演示】

如果你更喜欢通过视频了解本文,请点击观看我在GopherCon上的演讲 www.youtube.com/watch?v=KyuFeiG3Y6...

Go语言最强大的特性之一就是基于 Tony Hoare’s CSP 这篇论文实现的内置并发. Go在设计时就考虑了并发并允许我们构建复杂的并发管道。那你有没有想过,各种并发模式看起来是怎样的?

你一定想过。 我们多数情况下都会通过想象来思考问题. 如果我问你一个关于“1到100的数字”的问题,你脑子里就会下意识的出现一系列画面。例如,我会把它想象成一条从我开始的直线,从数字1到20然后右转90度一直到1000+。我记得我很小的时候,在我们的幼儿园里,衣帽间里有很多数字,写在墙上,数字20恰好在拐角处。你可能有你自己的关于数字的画面。另一个常见的例子是一年四季的视觉展现——有人将之想象成一个盒子,有人将之想象成一个圈。

无论如何, 我想用Go和WebGL把我对于常见的并发模式的具象化尝试展现给大家.这多多少少代表了我对于并发程序的理解。如果能听到我和大家脑海中的画面有什么不同,一定会非常有趣。 我特别想知道 Rob Pike 或者 Sameer Ajmani 脑子里是怎么描绘并发图像的. 我打赌我会很感兴趣的。【相关推荐:Go视频教程

那么,让我们从一个很基础的“Hello,Concurrent World”例子开始我们今天的主题。

Hello, Concurrent world

代码很简单——单个通道,单个goroutine,单次写入,单次读取。

package main

func main() {
    // 创建一个int类型的通道
    ch := make(chan int)

    // 开启一个匿名 goroutine
    go func() {
        // 向通道发送数字42
        ch <p><a href="https://divan.dev/demos/hello/" target="_blank">转到交互式 WebGL 动画</a> <img src="https://img.php.cn/upload/article/000/000/024/03104ecc960e1992d7b04ec4cd79a77f-0.gif" alt="Hello, World"></p><p>蓝色线代表随时间运行的goroutine. 连接‘main’和‘go #19’的蓝色细线用来标记goroutine的开始和结束同时展示了父子关系,最后,红线代表发送/接收动作. 虽然这是两个独立的动作,我还是尝试用“从 A 发送到 B”的动画将他们表示成一个动作. goroutine 名称中的“#19” 是 goroutine 真实的内部ID, 其获取方法参考了 Scott Mansfield 的 “Goroutine IDs” 这篇文章。</p><h3>
<span class="header-link octicon octicon-link"></span><strong><span style="font-size: 18px;">Timers</span></strong>
</h3><p>实际上,你可以通过以下方法构建一个简单的计时器——创建一个通道, 开启一个 goroutine 让其在指定的时间间隔后向通道中写入数据,然后将这个通道返回给调用者。于是调用函数就会在读取通道时阻塞,直到之前设定的时间间隔过去。接下来我们调用24次计时器然后尝试具象化调用过程。</p><pre class="brush:php;toolbar:false">package main

import "time"

func timer(d time.Duration) <p><a href="https://divan.dev/demos/timers/" target="_blank">转到交互式 WebGL 动画</a> <img src="https://img.php.cn/upload/article/000/000/024/6e2b2d910982ba58209c1c53cd9c7f6d-1.gif" alt="Recurrent Timers"></p><p>很整洁,对吗? 我们继续。</p><h3>
<span class="header-link octicon octicon-link"></span><strong><span style="font-size: 18px;">Ping-pong</span></strong>
</h3><p>这个并发例子取自谷歌员工 Sameer Ajmani “<a href="https://talks.golang.org/2013/advconc.slide#1" target="_blank" textvalue="Advanced Go Concurrency Patterns">Advanced Go Concurrency Patterns</a>” 演讲。当然,这个模式不算非常高级,但是对于那些只熟悉Go的并发机制的人来说它看起来可能非常新鲜有趣。</p><p>这里我们用一个通道代表乒乓球台. 一个整型变量代表球, 然后用两个goroutine代表玩家,玩家通过增加整型变量的值(点击计数器)模拟击球动作。</p><pre class="brush:php;toolbar:false">package main

import "time"

func main() {
    var Ball int
    table := make(chan int)
    go player(table)
    go player(table)

    table <p><a href="https://divan.dev/demos/pingpong/" target="_blank" textvalue="转到交互式 WebGL 动画">转到交互式 WebGL 动画</a> <img src="https://img.php.cn/upload/article/000/000/024/6e2b2d910982ba58209c1c53cd9c7f6d-2.gif" alt="Ping-Pong"></p><p>这里我建议你点击 <a href="https://divan.dev/demos/pingpong/" target="_blank">链接</a> 进入交互式 WebGL 动画操作一下. 你可以放慢或者加速动画,从不同的角度观察。</p><p>现在,我们添加三个玩家看看。</p><pre class="brush:php;toolbar:false">    go player(table)
    go player(table)
    go player(table)
登录后复制

转到交互式 WebGL 动画 Ping-Pong 3 我们可以看到每个玩家都按照次序轮流操作,你可能会想为什么会这样。为什么多个玩家(goroutine)会按照严格的顺序接到“球”呢。

答案是 Go 运行时环境维护了一个 接收者 FIFO 队列 (存储需要从某一通道上接收数据的goroutine),在我们的例子里,每个玩家在刚发出球后就做好了接球准备。我们来看一下更复杂的情况,加入100个玩家。

for i := 0; i <p><a href="https://divan.dev/demos/pingpong100/" target="_blank">转到交互式 WebGL 动画</a> <img src="https://img.php.cn/upload/article/000/000/024/f022e6361217c7f4c90e16d8d0885f99-4.gif" alt="Ping-Pong 100"> </p><p>先进先出顺序很明显了,是吧? 我们可以创建一百万个goroutine,因为它们很轻量,但是对于实现我们的目的来说没有必要。我们来想想其他可以玩的。 例如, 常见的消息传递模式。</p><h3>
<span class="header-link octicon octicon-link"></span><strong><span style="font-size: 18px;">Fan-In</span></strong>
</h3><p>并发世界中流行的模式之一是所谓的 <em>fan-in</em> 模式。这与 <em>fan-out</em> 模式相反,稍后我们将介绍。简而言之,fan-in 是一项功能,可以从多个输入中读取数据并将其全部多路复用到单个通道中。</p><p>举例来说:</p><pre class="brush:php;toolbar:false">package main

import (
    "fmt"
    "time"
)

func producer(ch chan int, d time.Duration) {
    var i int
    for {
        ch <p><a href="https://divan.dev/demos/fanin/" target="_blank">Go to interactive WebGL animation</a> <img src="https://img.php.cn/upload/article/000/000/024/f022e6361217c7f4c90e16d8d0885f99-5.gif" alt="Fan-In Pattern"></p><p>如我们所见,第一个 <em>producer</em> 每100毫秒生成一次值,第二个每250毫秒生成一次值,但是 <em>reader</em> 会立即从这两个生产者那里接受值。实际上,多路复用发生在 <em>main</em> 的range循环中。</p><h3>
<span class="header-link octicon octicon-link"></span><strong><span style="font-size: 18px;">Workers</span></strong>
</h3><p>与 <em>fan-in</em> 相反的模式是 <em>fan-out</em> 或者<em>worker</em> 模式。多个 goroutine 可以从单个通道读取,从而在CPU内核之间分配大量的工作量,因此是 <em>worker</em> 的名称。在Go中,此模式易于实现-只需以通道为参数启动多个goroutine,然后将值发送至该通道-Go运行时会自动地进行分配和复用 :)</p><pre class="brush:php;toolbar:false">package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(tasksCh <p><img src="https://img.php.cn/upload/article/000/000/024/bc82f8929dc05185db5d2b4542d2ca73-6.gif" alt="Go"></p><p>这里值得一提的是:并行性。如您所见,所有goroutine并行’运行‘,等待通道给予它们’工作‘。鉴于上面的动画,很容易发现goroutine几乎立即接连地收到它们的工作。不幸的是,该动画在goroutine确实在处理工作还是仅仅是在等待输入的地方没有用颜色显示出来,但是此动画是在GOMAXPROCS=4的情况下录制的,因此只有4个goroutine有效地并行运行。我们将很快讨论这个主题。</p><p>现在,让我们做一些更复杂的事情,并启动一些有自己workers(subworkers)的workers。</p><pre class="brush:php;toolbar:false">package main

import (
    "fmt"
    "sync"
    "time"
)

const (
    WORKERS    = 5
    SUBWORKERS = 3
    TASKS      = 20
    SUBTASKS   = 10
)

func subworker(subtasks chan int) {
    for {
        task, ok := <p><a href="https://divan.dev/demos/workers2/" target="_blank">Go to interactive WebGL animation</a> <img src="https://img.php.cn/upload/article/000/000/024/bc82f8929dc05185db5d2b4542d2ca73-7.gif" alt="Workers of workers"> 很好。当然,我们可以将worker和subworker的数量设置为更高的值,但是我试图使动画清晰易懂。</p><p>更酷的 fan-out 模式确实存在,例如动态数量的worker/subworker,通过通道发送通道,但是 fan-out 的想法现在应该很清楚了。</p><h3>
<span class="header-link octicon octicon-link"></span><strong><span style="font-size: 18px;">服务器</span></strong>
</h3><p>下一个常见的模式类似于扇出,但是会在很短的时间内生成goroutine,只是为了完成某些任务。它通常用于实现服务器-创建侦听器,循环运行accept()并为每个接受的连接启动goroutine。它非常具有表现力,可以实现尽可能简单的服务器处理程序。看一个简单的例子:</p><pre class="brush:php;toolbar:false">package main

import "net"

func handler(c net.Conn) {
    c.Write([]byte("ok"))
    c.Close()
}

func main() {
    l, err := net.Listen("tcp", ":5000")
    if err != nil {
        panic(err)
    }
    for {
        c, err := l.Accept()
        if err != nil {
            continue
        }
        go handler(c)
    }
}
登录后复制

Go to 交互式WebGL动画 Servers 

这不是很有趣-似乎并发方面没有发生任何事情。当然,在引擎盖下有很多复杂性,这是我们特意隐藏的。 “简单性很复杂”.

但是,让我们回到并发性并向我们的服务器添加一些交互。假设每个处理程序都希望异步写入记录器。在我们的示例中,记录器本身是一个单独的goroutine,它可以完成此任务。

package main

import (
    "fmt"
    "net"
    "time"
)

func handler(c net.Conn, ch chan string) {
    ch <p><a href="https://divan.dev/demos/servers2/" target="_blank">Go to 交互式WebGL动画</a> <img src="https://img.php.cn/upload/article/000/000/024/dfdd69afd5511ae20942d0af033a1db2-9.gif" alt="Servers 2"> </p><p>不是吗?但是很容易看到,如果请求数量增加并且日志记录操作花费一些时间(例如,准备和编码数据),我们的* logger * goroutine很快就会成为瓶颈。我们可以使用一个已知的扇出模式。我们开始做吧。</p><h3>
<span class="header-link octicon octicon-link"></span><strong><span style="font-size: 18px;">服务器+工作者</span></strong>
</h3><p>带工作程序的服务器示例是记录器的高级版本。它不仅可以完成一些工作,而且还可以通过* results *通道将其工作结果发送回池中。没什么大不了的,但是它将我们的记录器示例扩展到了更实际的示例。</p><p>让我们看一下代码和动画:</p><pre class="brush:php;toolbar:false">package main

import (
    "net"
    "time"
)

func handler(c net.Conn, ch chan string) {
    addr := c.RemoteAddr().String()
    ch <p><a href="https://divan.dev/demos/servers3/" target="_blank">Go to 交互式WebGL动画</a> <img src="https://img.php.cn/upload/article/000/000/024/d04fb172e580bf3c4b0bca0edac0a234-10.gif" alt="Server + Worker"> 我们在4个goroutine之间分配了工作,有效地提高了记录器的吞吐量,但是从此动画中,我们可以看到记录器仍然可能是问题的根源。成千上万的连接在分配之前会汇聚在一个通道中,这可能导致记录器再次成为瓶颈。但是,当然,它会在更高的负载下发生。</p><h3>
<span class="header-link octicon octicon-link"></span><strong><span style="font-size: 18px;">并发素筛(素筛指素数筛法)</span></strong>
</h3><p>足够的扇入/扇出乐趣。让我们看看更复杂的并发算法。我最喜欢的例子之一是Concurrent Prime Sieve,可以在[<a href="https://talks.golang.org/2012/concurrency.slide" target="_blank">Go Concurrency Patterns</a>]对话中找到。素数筛,或<a href="https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes" target="_blank">[Eratosthenes筛)</a>是一种古老的算法,用于查找达到给定限制的素数。它通过按顺序消除所有质数的倍数来工作。天真的算法并不是真正有效的算法,尤其是在多核计算机上。</p><p>该算法的并发变体使用goroutine过滤数字-每个发现的素数一个goroutine,以及用于将数字从生成器发送到过滤器的通道。找到质数后,它将通过通道发送到* main *以进行输出。当然,该算法也不是很有效,特别是如果您想找到大质数并寻找最低的Big O复杂度,但是我发现它非常优雅。</p><pre class="brush:php;toolbar:false">// 并发的主筛
package main

import "fmt"

// 将序列2、3、4,...发送到频道“ ch”。
func Generate(ch chan<- int) {
    for i := 2; ; i++ {
        ch <- i // Send &#39;i&#39; to channel &#39;ch&#39;.
    }
}

//将值从通道“ in”复制到通道“ out”,
//删除可被“素数”整除的那些。
func Filter(in <-chan int, out chan<- int, prime int) {
    for {
        i := <-in // Receive value from &#39;in&#39;.
        if i%prime != 0 {
            out <- i // Send &#39;i&#39; to &#39;out&#39;.
        }
    }
}

//主筛:菊花链过滤器过程。
func main() {
    ch := make(chan int) // Create a new channel.
    go Generate(ch)      // Launch Generate goroutine.
    for i := 0; i < 10; i++ {
        prime := <-ch
        fmt.Println(prime)
        ch1 := make(chan int)
        go Filter(ch, ch1, prime)
        ch = ch1
    }
}
登录后复制

转到交互式WebGL动画

PrimeSieve

,请以交互模式随意播放此动画。我喜欢它的说明性-它确实可以帮助您更好地理解该算法。 * generate * goroutine发出从2开始的每个整数,每个新的goroutine仅过滤特定的质数倍数-2、3、5、7 …,将第一个找到的质数发送给* main *。如果旋转它从顶部看,您会看到从goroutine发送到main的所有数字都是质数。漂亮的算法,尤其是在3D中。

GOMAXPROCS(调整并发的运行性能)

现在,让我们回到我们的工作人员示例。还记得我告诉过它以GOMAXPROCS = 4运行吗?那是因为所有这些动画都不是艺术品,它们是真实程序的真实痕迹。

让我们回顾一下GOMAXPROCS是什么。

GOMAXPROCS设置可以同时执行的最大CPU数量。

当然,CPU是指逻辑CPU。我修改了一些示例,以使他们真正地工作(而不仅仅是睡觉)并使用实际的CPU时间。然后,我运行了代码,没有进行任何修改,只是设置了不同的GOMAXPROCS值。 Linux机顶盒有2个CPU,每个CPU具有12个内核,因此有24个内核。

因此,第一次运行演示了该程序在1个内核上运行,而第二次-使用了所有24个内核的功能。

WebGL动画-1| WebGL动画-24GOMAXPROCS1

GOMAXPROCS24

这些动画中的时间速度是不同的(我希望所有动画都适合同一时间/ height),因此区别很明显。当GOMAXPROCS = 1时,下一个工作人员只有在上一个工作完成后才能开始实际工作。在GOMAXPROCS = 24的情况下,加速非常大,而复用的开销可以忽略不计。

不过,重要的是要了解,增加GOMAXPROCS并不总是可以提高性能,在某些情况下实际上会使它变得更糟。

Goroutines leak

我们可以从Go中的并发时间中证明什么呢?我想到的一件事情是goroutine泄漏。例如,如果您启动goroutine,但超出范围,可能会发生泄漏。或者,您只是忘记添加结束条件,而运行了for{}循环。

第一次在代码中遇到goroutine泄漏时,我的脑海中出现了可怕的图像,并且在下个周末我写了 expvarmon。现在,我可以使用WebGL可视化该恐怖图像。

看一看:

Go

仅仅是看到此,我都会感到痛苦:) 所有这些行都浪费了资源,并且是您程序的定时炸弹。

Parallelism is not Concurrency

我要说明的最后一件事是并行性与并发性之间的区别。这个话题涵盖了   很多 ,Rob Pike在这个话题上做了一个精彩的演讲。确实是#必须观看的视频之一。

简而言之,

并行是简单的并行运行事物。

并发是一种构造程序的方法。

因此,并发程序可能是并行的,也可能不是并行的,这些概念在某种程度上是正交的。我们在演示 GOMAXPROCS 设置效果时已经看到了这一点。

我可以重复所有这些链接的文章和谈话,但是一张图片相当于说了一千个字。我在这里能做的是可视化这个差异。因此,这是并行。许多事情并行运行。

转到交互式WebGL动画 并行 1

这也是并行性:

转到交互式WebGL动画

Go

但这是并发的:

PrimeSieve

还有这个:

Workers of workers

这也是并发的:

Go

How it was made

为了创建这些动画,我编写了两个程序:gotracergothree.js 库。首先,gotracer执行以下操作:

  • 解析Go程序的AST树(Abstract Syntax Tree,抽象语法树),并在与并发相关的事件上插入带有输出的特殊命令-启动/停止goroutine,创建通道,向/从通道发送/接收。
  • 运行生成的程序
  • 分析此特殊输出,并生成带有事件和时间戳描述的JSON。

生成的JSON示例:JSON sample

接下来,gothree.js使用令人惊叹的 Three.js 库的功能来使用WebGL绘制3D线和对象。我做了一些很小的包装使其适合单个html页面-就是这样。

但是,这种方法非常有局限。我必须准确地选择示例,重命名通道和goroutine,以使得或多或少复杂的代码产生正确的跟踪。使用这种方法,如果goroutine具有不同的名称,就没有简便的方法来关联goroutine之间的通道。更不用说通过chan类型的通道发送的通道。时序方面也存在很大的问题-输出到stdout可能比发送值花费更多的时间,因此在某些情况下,我必须放置time.Sleep(一些毫秒)以获取正确的动画。

基本上,这就是为什么我还没有开源代码的原因。我正在玩 Dmitry Vyukov 的 执行跟踪器,它似乎提供了事件的详细信息,但是没有包含发送值得信息。也许有更好的方法可以实现预期的目标。如果您有想法,请在twitter给我写信,或在评论中写给我。如果能将这个为期两周的工具扩展为任何Go程序的真正的调试/跟踪工具,那将是非常棒的。

我也很乐意看到此处未列出的更有趣的并发算法和模式。欢迎随时在评论中写一个。

Happy coding!

UPD: 可在 github.com/pan/gotrace 上使用此工具,并使用Go Execution Tracer和打了补丁的运行时生成跟踪。

另外,我愿意接受新工作,因此,如果您公司/团队对我感兴趣,有难题需要使用Go解决,我可以远程工作(或者您在巴塞罗那)并在招聘,请告诉我:)

英文原文地址:https://divan.dev/posts/go_concurrency_visualize/

更多编程相关知识,请访问:编程视频!!

以上就是一文详解Go中的并发【20 张动图演示】的详细内容,更多请关注php中文网其它相关文章!

智能AI问答
PHP中文网智能助手能迅速回答你的编程问题,提供实时的代码和解决方案,帮助你解决各种难题。不仅如此,它还能提供编程资源和学习指导,帮助你快速提升编程技能。无论你是初学者还是专业人士,AI智能助手都能成为你的可靠助手,助力你在编程领域取得更大的成就。
相关标签:
来源:learnku网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
最新问题
关于CSS思维导图的课件在哪? 课件
凡人来自于2024-04-16 10:10:18
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2024 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号