Go并发&调度亲和性

【译文】原文地址
将goroutine从一个操作系统线程切换到另一个线程是有代价的,如果切换太频繁会降低应用程序的速度。随着Go发展,调度器已经解决了这个问题。在并发工作时,调度器提供goroutine和线程的亲和性。先回顾几年前的调度器来理解这种改进过程。

Go老版本存在的问题

在Go 1.0&1.1早期,当创建更多os线程(即将GOMAXPROCS值设置更大)来运行并发程序时将会面临性能下降的问题。让我们从文档中使用channel来计算质数的例子开始:

package main

import "fmt"

//Send the sequence 2, 3, 4, ... to channel 'ch'.
func Generate(ch chan<- int)  {
    for i := 2; ;i++  {
        ch <- i
    }
}

//Copy the values from channel 'in' to channel 'out',
//removing those divisable by 'prime'.
func Filter(in <-chan int, out chan<- int, prime int)  {
    for{
        i := <-in  //Receive value from 'in'.
        if i % prime !=0 {
            out <- i  //send 'i' to 'out'.
        }
    }
}

// The prime sieve: Daisy-chain Filter processes.
func main() {
    ch := make(chan int)
    go Generate(ch)
    for i :=0; i <10; i++{
        prime := <-ch
        fmt.Println(prime)
        ch1 := make(chan int)
        go Filter(ch, ch1, prime)
        ch = ch1

    }

}

以下是Go 1.0.3版本,在不同GOMAXPROCS值下,计算10万个质数的基准测试结果:

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

要理解这些结果,我们需要理解此时调度器是如何设计的。在Go第一个版本中,调度器只有一个全局队列,所有的线程都可以推送和获取goroutines。下面是一个应用程序实例,该应用程序最多运行两个操作系统线程M,通过设置GOMAXPROCS=2来实现:


第一个版本调度器只要一个全局队列

只有一个队列并不能保证goroutine将在同一个线程上恢复执行。第一个线程准备就绪,会获取一个等待的goroutine运行。因此,这里就会涉及到goroutine从一个线程到另一个线程的切换,在性能方面会产生消耗。下面是一个阻塞式channel例子:

  • G7协程阻塞在channel上,等待channel中发送来的数据。一旦channel有数据可接收,该协程会被推送到全局队列当中。


  • 然后,channel推送消息,GX协程将在一个准备就绪的线程上运行,而G8协程将阻塞在channel上:


  • 此时 G7协程就会被调度到该线程上去:



    Goroutine现在在不同的线程上运行。只有一个全局调度队列会迫使调度程序只有一个互斥锁来覆盖所有的goroutine调度操作。以下是使用pprof工具获取的CPU概况:

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

procyield, xchg, futex和lock都与Go调度器的全局互斥量有关。很清楚的发现,应用程序的很大部分时间花在锁上。
这些问题导致Go在多处理器上没有优势,在Go1.1中已经通过一个新的调度器解决了。

并发时的亲和性

Go 1.1实现了一个新的调度器,并创建了本地调度队列。如果有本地goroutines调度队列并允许他们运行在同一个OS线程上,这个改进避免了锁定整个调度程序。
由于线程可能在系统调用时阻塞,并且这种阻塞的线程数量是没有限制的,Go引入了processes的概念。处理器P表示代表一个运行的OS线程并管理本地goroutine调度队列。下面是新的模式:



如下是在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的操作只与channels相关。但是,如果调度程序改进了goroutine和线程之间的亲和性,那么在某些情况下,这种亲和性需要降低。

限制亲和性

要了解亲和性的限制,我们必须了解何时会进入本地队列和全局队列。本地队列将用于除了系统调用外的所有操作,例如阻塞在通道上和select操作,以及等待计时器和锁,goroutine都会进入本地调度队列。然而,有两个特例可以限制goroutine和线程的亲和性:

  • 工作窃取。当处理器P在本地队列没有足够的goroutine可调度,将会从其他P中窃取,并且全局队列和网络轮询都为空。被窃取的goroutine就会在别的线程执行。
  • 系统调用。当发生系统调用(如文件操作,http调用,数据库操作等),Go以阻塞模式挂起正在运行的os线程,让新的线程来处理当前P上的本地队列。
    但是,为了更好地管理本地队列优先级,以上两个约束可以避免。Go 1.5为了给goroutine在channel上来回通信提供更多的优先级,因此通过指定线程以优化亲和性。

排序来提高亲和性

goroutine在通道上来回通信导致频繁的阻塞,例如频繁在本地队列中排队。然而,由于本地队列有一个FIFO实现,未阻塞的goroutine不能保证马上得到运行,如果线程被其他goroutine占用。下面是一个关于一个之前被channel阻塞但现在可执行的goroutine例子:



G9在被channel阻塞后恢复。但是,它必须等G2、G5和G4才能执行。在这个例子中,G5将占用线程导致G9延迟执行,会导致G9被其他处理器窃取的风险。从Go 1.5开始,从通道中恢复的goroutine将优先被执行,这主要归功于P的一个特殊属性:



G9现在被标记为下一个可执行goroutine。这个新的优先级允许goroutine从通道中恢复就马上得到执行。然后其他goroutine现在将有运行时间。这一改变总体对Go标准库产生积极影响,改善了一些包的性能。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容