前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#59 Not understanding the concurrency impacts of ..

Go语言中常见100问题-#59 Not understanding the concurrency impacts of ..

作者头像
数据小冰
发布2022-08-15 15:22:29
2510
发布2022-08-15 15:22:29
举报
文章被收录于专栏:数据小冰数据小冰
不清楚工作负载类型对并发的影响

本节内容将讨论计算机工作负载类型对并发的影响。事实上,如果工作负载受CPU或IO限制,可能有不同的处理方法。现在先弄清楚这些概念,然后深入研究它的影响。

程序执行的时间受到下面因素的限制:

  • CPU的速度,例如归并排序算法,负载类型为CPU密集型
  • I/O的速度,例如在数据库中进行REST调用或查询,负载类型为I/O密集型
  • 可用内存,负载类型为内存密集型

鉴于过去几十年内存变得非常便宜,所以现在负载类型为内存密集型很少见。本节将重点介绍前两种工作负载类型:CPU密集型和I/O密集型

为什么说在并发应用程序中,工作负载类型对程序有很大影响呢? 下面通过工作池这种并发模式来理解。

下面的实例中,读取函数从一个io.Reader中不断的读取1024个字节,并将读取的内容传给任务函数task,task将执行一些操作之后返回一个整数。read统计所有的返回整数之和,下面是顺序实现。

代码语言:javascript
复制
func read(r io.Reader) (int, error) {
        count := 0
        for {
                b := make([]byte, 1024)
                _, err := r.Read(b)
                if err != nil {
                        if err == io.EOF {
                                break
                        }
                        return 0, err
                }
                count += task(b)
        }
        return count, nil
}

如果我们想以并行的方式运行task任务,该怎么实现呢?一种处理方法是使用俗称的工作池模式。工作池模式是先创建固定数量的工作程序(goroutine),这些工作goroutine将从一个公共channel中轮询处理任务,如下图所示。

首先,启动一个固定数量的goroutine池,具体数量多少在后面讨论。然后,创建一个共享的channel.在每次从io.Reader读取到数据之后,将数据发送到channel上,池中的每个goroutine从这个channel中接收数据,执行task操作,最后更新共享计数器的值。

下面是Go语言的一个可能实现,先启动10个goroutine,每个goroutine在处理完成之后自动更新共享计数器count值。

代码语言:javascript
复制
func read(r io.Reader) (int, error) {
        var count int64
        wg := sync.WaitGroup{}
        var n = 10

        ch := make(chan []byte, n)
        wg.Add(n)
        for i := 0; i < n; i++ {
                go func() {
                        defer wg.Done()
                        for b := range ch {
                                v := task(b)
                                atomic.AddInt64(&count, int64(v))
                        }
                }()
        }

        for {
                b := make([]byte, 1024)
                // Read from r to b
                ch <- b
        }
        close(ch)
        wg.Wait()
        return int(count), nil
}

上面的代码中,变量n表示池子的大小,并创建了缓冲区也为n的channel. 启动了n个子goroutine,这样可以减少main goroutine在发送消息的时候潜在的争用,在子goroutine中,从通道中读取数据,并执行task操作,最后原子的更新计数器的值。在main goroutine中,从io.Reader中读取数据并将任务发送到通道中。最后关闭通道,并等待所有子goroutine的退出之后才返回。

设置有固定数量的goroutine有这些优点:它减少了资源的影响,以及对外部系统的影响。现在问题来了,n的值要设置为多少比较合适?答案是取决于程序任务的负载类型。

如果工作负载是I/O密集型的,n的值设定主要取决于外部系统,如果我们想最大化吞吐量,系统将处理多少并发访问?

如果工作是CPU密集型的,最佳实践是依赖于GOMAXPROCS。GOMAXPROCS是一个变量,用于设置分配给正在运行的goroutine的OS线程数。默认情况下,此值设置为逻辑CPU的数量。

NOTE: 可以使用runtime.GOMAXPROCS(int)函数来更新GOMAXPROC的值,如果传0给此函数,只会返回当前GOMAXPROCS的值并不会改变GOMAXPROCS的值。

代码语言:javascript
复制
// 不会改变GOMAXPROCS的值
n := runtime.GOMAXPROCS(0)

将池子的大小映射为GOMAXPROCS的原因是啥呢?下面通过一个具体的例子进行说明,假设程序将在4核的机器上运行。因此Go运行时将实例化4个OS线程,用来执行goroutine.起初,可能会遇到这样的场景,有4个CPU内核和四个goroutine,但是只有一个被执行。如下图所示:

M0当前正在运行工作池中的goroutine. 因此,这些goroutine开始从通道接收消息并执行它的任务。但是,池中的其他三个goroutine尚未分配给M,它们现在处于可运行状态。M1、M2和M3没有任何goroutine可以运行,所以它们没有与内核关联。只有一个goroutine正在运行。

最终,根据Go运行时调度机制,会进行工作窃取,P1可能会从本地P0队列中窃取goroutine, 效果如下图所示。

上图中P1从P0窃取了三个goroutine.在这种情况下,最终Go调度程序可能会将所有goroutine分配给不同的OS线程。然而,不能保证这应该在什么时候发生。由于Go调度程序的主要目标之一是优化资源分配(goroutine的分布),考虑到工作负载的性质,最终应该处于这样的场景中。

但是,上述情况仍然不是最优的,因为最多只有两个goroutine正在运行。假设机器只运行我们的程序(操作系统进程除外),所以P2和P3都是空闲的。因此最终,操作系统可能会如下图所示的方式移动M2和M3。

现在上述情况已成为最佳状态,四个goroutine在单独的线程中运行。每个线程都在单独的核上运行。这会减少goroutine和线程级别上下文的切换。虽然Go程序开发人员不能通过设计程序让它以上图期望的方式执行。但是在CPU密集型的工作负载中,可以设置工作池的大小为GOMAXPROCS,这样有利于程序达到或接近上述状态。

NOTE: 在特定的条件下,如果我们希望goroutine的数量绑定到CPU内核的数量,那为什么不将它设置为runtime.NumCPU()的值呢?这个函数不是返回的是逻辑CPU内核的数量吗?正如前面提到的,GOMAXPROCS可以更改并且值可能低于CPU内核的数量。在CPU密集工作负载的情况下,如果核心数为4,但我们只有3个线程,不应该启动4个线程,而是应该启动3个线程。否则,线程将在两个goroutine之间共享其执行时间,从而会增加上下文切换的次数。

在工作池模式代码的编写过程中,可以看到池子的最佳goroutine数量取决于工作负载类型。如果worker执行的工作负载是IO密集型的,那么goroutine的数量取决于外部系统,相反,如果工作类型是CPU密集型的,goroutine的数量设置为接近可用线程的数量。这就是说为什么在设计并发应用程序时,了解任务的负载类型非常重要的原因。需要注意,在大多数情况下,我们应该通过基准测试来验证我们的假设,并发性并不简单,很容易草率做出最终可能与预期的不一致的情况。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 不清楚工作负载类型对并发的影响
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档