Go:并发和调度程序亲和性

iyacontrol · · 1180 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

g-01.png

将一个goroutine从一个OS线程切换到另一个线程是有成本的,并且如果这种情况发生得太频繁,可能会使应用程序变慢。但是,随着时间的流逝,Go调度程序已经解决了这个问题。现在,当并发工作时,它可以在goroutine和线程之间提供关联。让我们回溯几年前来了解这种改进。

原始问题

在Go的早期,比如Go 1.0和1.1,当使用更多OS线程(即,更高的GOMAXPROCS值)运行并发代码时,该语言将面临性能下降的问题。让我们从计算素数的文档中使用一个示例开始:

g-02.png

这是使用多个GOMAXPROCS值计算前十万个素数时Go 1.0.3的基准:

name time/op  
Sieve 19.2s ± 0%  
Sieve-2 19.3s ± 0%  
Sieve-4 20.4s ± 0%  
Sieve-8 20.4s ± 0%

要了解这些结果,我们需要了解此时如何设计调度程序。在Go的第一个版本中,调度程序只有一个全局队列,所有线程都可以在其中推送并获取goroutine。这是一个应用程序的示例,该应用程序最多将两个操作系统线程(以下架构中的M)运行(通过将GOMAXPROCS设置为两个来定义):

g-03.png

仅具有一个队列并不能保证goroutine将在同一线程上恢复。准备就绪的第一个线程将提取一个等待的goroutine并将其运行。因此,它涉及将goroutines从一个线程转移到另一个线程,并且在性能方面代价很高。这是一个带有阻塞通道的示例:

  • Goroutine#7在通道上阻塞,正在等待消息。收到消息后,goroutine将推入全局队列:

g-04.png

  • 然后,通道推送消息,并且goroutine #X将在可用线程上运行,而goroutine#8在通道上阻塞:

g-05.png

  • goroutine#7现在在可用线程上运行:

g-06.png

现在,goroutine在不同的线程上运行。具有单个全局队列也将迫使调度程序具有一个覆盖所有goroutines调度操作的单个全局互斥量。这是使用pprof创建的CPU配置文件,其中GOMAXPROCS设置为height:

Total: 8679 samples  
3700 42.6% 42.6% 3700 42.6% runtime.procyield  
1055 12.2% 54.8% 1055 12.2% runtime.xchg  
753 8.7% 63.5% 1590 18.3% runtime.chanrecv  
677 7.8% 71.3% 677 7.8% dequeue  
438 5.0% 76.3% 438 5.0% runtime.futex  
367 4.2% 80.5% 5924 68.3% main.filter  
234 2.7% 83.2% 5005 57.7% runtime.lock  
230 2.7% 85.9% 3933 45.3% runtime.chansend  
214 2.5% 88.4% 214 2.5% runtime.osyield  
150 1.7% 90.1% 150 1.7% runtime.cas

procyieldxchgfutexlock都与Go调度程序的全局互斥量有关。我们清楚地看到,应用程序将大部分时间都花在了锁定上。

这些问题不允许Go发挥处理器的优势,并且在Go 1.1中使用新的调度程序解决了这些问题。

并发中的亲和性

Go 1.1附带了新调度程序的实现和本地goroutine队列的创建。如果存在本地goroutine,此改进避免了锁定整个调度程序,并允许它们在同一OS线程上工作。

由于线程可以阻塞系统调用,并且不受限制的线程数没有限制,因此Go引入了处理器的概念。处理器P代表一个正在运行的OS线程,它将管理本地goroutine队列。这是新的架构:

g-08.png

这是Go 1.1.2中新计划程序的新基准:

name time/op  
Sieve 18.7s ± 0%  
Sieve-2 8.26s ± 0%  
Sieve-4 3.30s ± 0%  
Sieve-8 2.64s ± 0%

Go现在真正利用了所有可用的CPU。 CPU配置文件也已更改:

Total: 630 samples  
163 25.9% 25.9% 163 25.9% runtime.xchg  
113 17.9% 43.8% 610 96.8% main.filter  
93 14.8% 58.6% 265 42.1% runtime.chanrecv  
87 13.8% 72.4% 206 32.7% runtime.chansend  
72 11.4% 83.8% 72 11.4% dequeue  
19 3.0% 86.8% 19 3.0% runtime.memcopy64  
17 2.7% 89.5% 225 35.7% runtime.chansend1  
16 2.5% 92.1% 280 44.4% runtime.chanrecv2  
12 1.9% 94.0% 141 22.4% runtime.lock  
9 1.4% 95.4% 98 15.6% runqput

与锁定相关的大多数操作已被删除,标记为chanXXXX的操作仅与通道相关。但是,如果调度程序改善了goroutine和线程之间的亲和力,则在某些情况下可以减少这种亲和力。

亲和性限制

要了解亲和性的限制,我们必须了解对本地和全局队列的处理。本地队列将用于所有需要系统调用的操作,例如阻塞通道和选择的操作,等待计时器和锁定。但是,两个功能可能会限制goroutine和线程之间的关联:

  • Worker抢夺。当处理器 P 的本地队列中没有足够的worker时,如果全局队列和网络轮询器为空,它将从其他 P 窃取goroutine。当被抢夺时,goroutine将在另一个线程上运行。
  • 系统调用。当发生系统调用时(例如文件操作,http调用,数据库操作等),Go会将运行中的OS线程移入阻塞模式,让新线程处理当前P上的本地队列。

但是,通过更好地管理本地队列的优先级,可以避免这两个限制。 Go 1.5旨在为goroutine在通道上来回通信提供更高的优先级,从而优化与分配的线程的亲和力。

为了增强亲和力

如前所述,在通道上来回通信的goroutine会导致频繁的阻塞,即在本地队列中频繁地重新排队。但是,由于本地队列具有FIFO实现,因此,如果另一个goroutine正在占用线程,则unblock goroutine不能保证尽快运行。这是一个goroutine的示例,该例程现在可以运行并且以前在通道上被阻止:

g-07.png

Goroutine#9在通道上被阻塞后恢复。但是,它必须在运行之前等待#2,#5和#4。在此示例中,goroutine#5将占用其线程,从而延迟goroutine#9,并使之处于被其他处理器窃取的危险中。从Go 1.5开始,由于其 P 的特殊属性,从阻塞通道返回的goroutine现在将优先运行:

g-10.png

Goroutine#9现在被标记为下一个可运行的。这种新的优先级划分功能使goroutine可以在再次被阻塞之前迅速运行。然后,其他goroutine将具有运行时间。此更改对Go标准库改善了某些软件包的性能产生了总体积极影响。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:iyacontrol

查看原文:Go:并发和调度程序亲和性

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1180 次点击  
加入收藏 微博
下一篇:招聘啦!
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传