golang调度器的一个陷阱

让我们快速进入问题,不浪费时间。试着执行下面的golang代码片段。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    var x int
    threads := runtime.GOMAXPROCS(0)
    println(threads)
    for i := 0; i < threads; i++ {
        go func() {
            for {
                x++
            }
        }()
    }
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

运行代码

$ GOMAXPROCS=8 go run x.go

(旁注:熟悉Golang的同道想必知道GOMAXPROCS其实对应的CPU核心数,也就是线程数,这里应该是原作者运行示例时的计算机的CPU核数为8,因为根据文档定义,如果runtime.GOMAXPROCS(0)传入参数小于1,如果特殊指定,GOMAXPROCS就等于CPU核心数)

你观察到程序从未终止吗?这就是我说的golang陷阱。如果你用C/C++写同样的程序,你就不会发现这样的问题。现在让我们修改程序,修改以下一行:

threads := runtime.GOMAXPROCS(0)-1

所以,我们只是减少了1个go协程的数量。如果你在这个改变后重新运行程序,你会发现程序正确地终止,并打印出结果。这非常令人惊讶,不是吗?要了解这个问题背后的原因,我们需要了解一下golang运行时和调度器的实现。

揭开调度器的神秘面纱

Golang提供了用于并发的goroutine。它们类似于线程,但它们是轻量级的,开销非常小。拥有数万个goroutine的程序并不罕见,而拥有一万个pthreads代价就非常高了。golang在用户态中实现了goroutine。golang运行时为go程序创建的操作系统线程(pthreads)等于GOMAXPROCS的数量。Go协程被golang运行时安排在这些有限的OS线程上。

操作系统调度器

让我们回顾一下操作系统是如何调度进程的。通常情况下,操作系统调度器会保存一份操作系统进程的列表,它们处于正在运行、可运行或不可运行的状态。如果一个进程的运行时间超过了调度器的时间片,它就会抢占该进程,并安排在同一CPU上执行另一个可运行的进程。抢占是通过定时器中断来实现的。定时器中断的频率为调度器时间片的间隔。在一段代码中正在执行的进程会停止执行,保存进程执行上下文并执行中断处理程序。中断处理程序会将执行切换到调度器中。现在,调度器可以决定在这个CPU上执行哪个可运行的进程。调度器会选择一个进程并切换到它的执行上下文。

Golang的调度器

Golang实现了一个可协作的抢占式调度器。它没有实现基于定时器中断的抢占。但是,这个调度器应该方便在一个OS线程上同时运行多个goroutine。Golang在运行时提供的构造体、库和系统调用(?此处翻译的不好,构造体这个说法听着怪怪的)中加入钩子,可以与调度器进行协作。由于它避开了调用进入调度器的计时器,所以将运行时提供的函数作为进入调度器的入口。如果我们设法写一个不使用任何运行时提供的封装函数的goroutine,会发生什么?这正是这里发生的事情。那个goroutine不会调用到调度器,并导致goroutine的抢占。

在上面的程序中,我们执行的goroutine等于GOMAXPROCS(操作系统线程)。主协程是一个额外的goroutine。每个go协程都运行一个无限循环,并带有一个整数增量操作,这为协程提供了没有调用到调度器的范围。因此,所有六个线程(GOMAXPROCS)都在运行无限循环,它们永远不会抢占。处于可运行状态的主协程无法执行,因为这六个线程中的任何一个线程都忙于执行无限循环,所以调度器永远不会被执行。当我们减少1个线程时,现在有一个OS线程变得空闲,能够执行主程序。

(旁注:假设系统是8个CPU,我们GOMAXPROCS减1以后运行程序,就会有一个核是空闲的,此时正好可以进入主线程中执行,虽然原作者这里写的是6,不过我觉得处于无限循环的线程应该等同于threads,当threads等于系统CPU核心数时,由于无限循环,主协程没有机会被调度到,所以就程序没法退出,当将threads头1时,主协程才有机会能够执行,GOMAXPROCS限制的是goroutine的最大并发能力,这个也是由golang自己的调度器实现的,那主协程能运行是由于golang调度所致吗?此处先埋下伏笔。
我分别在不同的go版本下运行了示例程序:1.13、1.14,得到了不同的结果,1.13符合预期,但是1.14下程序却有不同表现,主线程总能得到执行,我想这应该是因为1.14版本的go调度器有较大变化所致,此处先埋点,后开坑)

在现实世界的程序中,这种情况是不太可能发生的,因为我们可能会使用运行时提供的功能,如channelssystemcallsfmt.SprintMutextime.Sleep至少一次。你可以在无限循环中添加一个无害的time.Sleep(0),然后观察程序不再挂起。

结论

虽然出现这个问题的几率非常小,但还是有可能发生。解决这个问题的方法是在这种情况下,从程序中强行调用进入调度器。runtime.Gosched()的调用有利于强制进入调度器。

这篇博文的灵感来自于我的同事,他在玩golang的时候就遇到了这个问题。

(旁注:此文非常有意思,一个简单的示例,却可以发散读者对于调度器的理解,有你当然,这篇文章还没有讲解goroutine为什么轻量,这是另一个有意思的话题,欢迎一起讨论)


原文链接 pitfall-of-golang-scheduler

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

推荐阅读更多精彩内容

  • 该文章主要详细具体的介绍Goroutine调度器过程及原理,可以对Go调度器的详细调度过程有一个清晰的理解,花 ...
    刘丹冰Aceld阅读 11,055评论 5 44
  • 前言 随着服务器硬件迭代升级,配置也越来越高。为充分利用服务器资源,并发编程也变的越来越重要。在开始之前,需要了解...
    云爬虫技术研究笔记阅读 3,735评论 0 7
  • 前言 相信听说go这门语言的同学都知道go在并发方面相对其它语言而言更突出,并发是所有的语言都有的功能,而为什么g...
    wp_nine阅读 996评论 0 1
  • 概要 本文从几个角度入手,描述和学习调度器原理 讲解调度器的基本概念 go语言的作者实现的C的协程库 libtas...
    zengfan阅读 6,231评论 0 21
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,454评论 16 22