关于scheduler:使用ScheduledExecutorService实现延时任务延时发布视频

应用ScheduledExecutorService能够实现定时工作(例如定时公布的性能) 先在类中定义局部变量 ScheduledExecutorService service = Executors.newScheduledThreadPool(50);Executors.newScheduledThreadPool(50); 此处应用了工厂模式。 工厂模式次要是为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到进步灵活性的目标。 @PostMapping("/ops/scheduled/publish") public ResponseResult scheduledPublish(@RequestBody ScheduleVideoDto dto) { List<Integer> vids = dto.getVids(); if (vids.isEmpty()){ return ResponseResult.of().withErrorMessage("公布视频失败,请抉择视频进行公布"); } Date pushTime = dto.getPushTime(); if (pushTime==null){ return ResponseResult.of().withErrorMessage("公布视频失败,请从新抉择公布工夫"); } for (int i = 0; i< vids.size();i++){ int status = videoService.getStatusById(vids.get(i)); if (status==1) vids.remove(vids.get(i)); } if (vids.isEmpty()){ return ResponseResult.of().withErrorMessage("公布视频失败,所选视频均为已公布"); } long delay = pushTime.getTime() - System.currentTimeMillis(); vids.forEach(vid->{ videoService.updatePushTime(vid,pushTime); service.schedule(() -> videoService.publish(vid), delay, TimeUnit.MILLISECONDS); }); return ResponseResult.of(); }在接口传入的dto中传入公布工夫PushTime ...

January 31, 2022 · 1 min · jiezi

关于scheduler:深入解析Scheduler

Kubernetes以其先进的理念、沉闷的社区,已成为以后容器集群化编排、部署和运行的事实标准。越来越多的企业和团队将Kubernetes引入到本人的研发和生产环境。 Scheduler是Kubernetes的重要组件,在整个零碎中承当了“承前启后”的重要性能,其外围的作用是将待调度的Pod依照特定的调度算法和调度策略绑定(Binding)到集群中某个适合的Node上,并将绑定信息写入etcd中。 8月26日晚8点,JFrog中国资深解决方案架构师高欣,将深刻、粗疏地剖析Scheduler的次要调度办法,及其运行机制,并通过实操演示的形式,来解说Scheduler如何通过这些调度办法实现Pod的无效调度。 你将学习到:1、Scheduler的工作原理2、Scheduler的次要调度办法3、实操演示Scheduler的调度过程 你将播种:1、深刻了解Scheduler的运行机制2、相熟和把握Scheduler的各种调度办法3、应用Scheduler来实现Pod的无效调度 参加形式点击下方链接报名即可 深刻解析Scheduler

August 24, 2021 · 1 min · jiezi

cron表达式基础讲解

初识 corncorn 表达式是一个字符串,分为 6 或 7 个域,每个域都会代表一个含义。 语法格式6 个域:second minute hour day month week7 个域:second minute hour day month week year由上可见,7 个域与 6 个域的语法只差了 year,一般情况下,我们使用 6 个域的结构。 corn表达式结构corn 从左到右(中间用空格隔开):秒 分 小时 月份中的日 月份 星期中的日期 年份 各个字段的含义 位置时间域名允许值允许的特殊字符1秒0-59, - * /2分0-59, - * /3小时0-23, - * /4日1-31, - * ? / L W C5月0-12, - * / 6星期1-7, - * ? / L C #7年(可选)1970-2099, - * /代码示例声明:本次的 cron 讲解结合了 springboot 框架。由于只是讲解 cron 表达式,所以就不在介绍 springboot 中如何使用 @Scheduled 注解的相关配置了秒的位置允许值为 0-59,如果写 60,将会报错 ...

July 7, 2019 · 2 min · jiezi

Go调度器系列(4)源码阅读与探索

各位朋友,这次想跟大家分享一下Go调度器源码阅读相关的知识和经验,网络上已经有很多剖析源码的好文章,所以这篇文章不是又一篇源码剖析文章,注重的不是源码分析分享,而是带给大家一些学习经验,希望大家能更好的阅读和掌握Go调度器的实现。本文主要分2个部分:解决如何阅读源码的问题。阅读源码本质是把脑海里已经有的调度设计,看看到底是不是这么实现的,是怎么实现的。带给你一个探索Go调度器实现的办法。源码都到手了,你可以修改、窥探,通过这种方式解决阅读源码过程中的疑问,验证一些想法。比如:负责调度的是g0,怎么才能schedule()在执行时,当前是g0呢?如何阅读源码阅读前提阅读Go源码前,最好已经掌握Go调度器的设计和原理,如果你还无法回答以下问题:为什么需要Go调度器?Go调度器与系统调度器有什么区别和关系/联系?G、P、M是什么,三者的关系是什么?P有默认几个?M同时能绑定几个P?M怎么获得G?M没有G怎么办?为什么需要全局G队列?Go调度器中的负载均衡的2种方式是什么?work stealing是什么?什么原理?系统调用对G、P、M有什么影响?Go调度器抢占是什么样的?一定能抢占成功吗?建议阅读Go调度器系列文章,以及文章中的参考资料:Go调度器系列(1)起源Go调度器系列(2)宏观看调度器Go调度器系列(3)图解调度原理优秀源码资料推荐既然你已经能回答以上问题,说明你对Go调度器的设计已经有了一定的掌握,关于Go调度器源码的优秀资料已经有很多,我这里推荐2个:雨痕的Go源码剖析六章并发调度,不止是源码,是以源码为基础进行了详细的Go调度器介绍:ttps://github.com/qyuhen/bookGo夜读第12期,golang中goroutine的调度,M、P、G各自的一生状态,以及转换关系:https://reading.developerlear…Go调度器的源码还涉及GC等,阅读源码时,可以暂时先跳过,主抓调度的逻辑。另外,Go调度器涉及汇编,也许你不懂汇编,不用担心,雨痕的文章对汇编部分有进行解释。最后,送大家一幅流程图,画出了主要的调度流程,大家也可边阅读边画,增加理解,高清版可到博客下载(原图原文跳转)。如何探索调度器这部分教你探索Go调度器的源码,验证想法,主要思想就是,下载Go的源码,添加调试打印,编译修改的源文件,生成修改的go,然后使用修改go运行测试代码,观察结果。下载和编译GoGithub下载,并且换到go1.11.2分支,本文所有代码修改都基于go1.11.2版本。$ GODIR=$GOPATH/src/github.com/golang/go$ mkdir -p $GODIR$ cd $GODIR/..$ git clone https://github.com/golang/go.git$ cd go$ git fetch origin go1.11.2$ git checkout origin/go1.11.2$ git checkout -b go1.11.2$ git checkout go1.11.2初次编译,会跑测试,耗时长一点$ cd $GODIR/src$ ./all.bash以后每次修改go源码后可以这样,4分钟左右可以编译完成$ cd $GODIR/src$ time ./make.bashBuilding Go cmd/dist using /usr/local/go.Building Go toolchain1 using /usr/local/go.Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.Building Go toolchain2 using go_bootstrap and Go toolchain1.Building Go toolchain3 using go_bootstrap and Go toolchain2.Building packages and commands for linux/amd64.—Installed Go for linux/amd64 in /home/xxx/go/src/github.com/golang/goInstalled commands in /home/xxx/go/src/github.com/golang/go/binreal 1m11.675suser 4m4.464ssys 0m18.312s编译好的go和gofmt在$GODIR/bin目录。$ ll $GODIR/bintotal 16044-rwxrwxr-x 1 vnt vnt 13049123 Apr 14 10:53 go-rwxrwxr-x 1 vnt vnt 3377614 Apr 14 10:53 gofmt为了防止我们修改的go和过去安装的go冲突,创建igo软连接,指向修改的go。$ mkdir -p ~/testgo/bin$ cd /testgo/bin$ ln -sf $GODIR/bin/go igo最后,把/testgo/bin加入到PATH,就能使用igo来编译代码了,运行下igo,应当获得go1.11.2的版本:$ igo versiongo version go1.11.2 linux/amd64当前,已经掌握编译和使用修改的go的办法,接下来就以1个简单的例子,教大家如何验证想法。验证schedule()由g0执行阅读源码的文章,你已经知道了g0是负责调度的,并且g0是全局变量,可在runtime包的任何地方直接使用,看到schedule()代码如下(所在文件:$GODIR/src/runtime/proc.go):// One round of scheduler: find a runnable goroutine and execute it.// Never returns.func schedule() { // 获取当前g,调度时这个g应当是g0 g := getg() if g.m.locks != 0 { throw(“schedule: holding locks”) } // m已经被某个g锁定,先停止当前m,等待g可运行时,再执行g,并且还得到了g所在的p if g.m.lockedg != 0 { stoplockedm() execute(g.m.lockedg.ptr(), false) // Never returns. } // 省略…}问题:既然g0是负责调度的,为何schedule()每次还都执行_g_ := getg(),直接使用g0不行吗?schedule()真的是g0执行的吗?在《Go调度器系列(2)宏观看调度器》这篇文章中我曾介绍了trace的用法,阅读代码时发现使用debug.schedtrace和print()函数可以用作打印调试信息,那我们是不是可以使用这种方法打印我们想获取的信息呢?当然可以。另外,注意print()并不是fmt.Print(),也不是C语言的printf,所以不是格式化输出,它是汇编实现的,我们不深入去了解它的实现了,现在要掌握它的用法:// The print built-in function formats its arguments in an// implementation-specific way and writes the result to standard error.// Print is useful for bootstrapping and debugging; it is not guaranteed// to stay in the language.func print(args …Type)从上面可以看到,它接受可变长参数,我们使用的时候只需要传进去即可,但要手动控制格式。我们修改schedule()函数,使用debug.schedtrace > 0控制打印,加入3行代码,把goid给打印出来,如果始终打印goid为0,则代表调度确实是由g0执行的:if debug.schedtrace > 0 { print(“schedule(): goid = “, g.goid, “\n”) // 会是0吗?是的}schedule()如下:// One round of scheduler: find a runnable goroutine and execute it.// Never returns.func schedule() { // 获取当前g,调度时这个g应当是g0 g := getg() if debug.schedtrace > 0 { print(“schedule(): goid = “, g.goid, “\n”) // 会是0吗?是的 } if g.m.locks != 0 { throw(“schedule: holding locks”) } // …}编译igo:$ cd $GODIR/src$ ./make.bash编写一个简单的demo(不能更简单):package mainfunc main() {}结果如下,你会发现所有的schedule()函数调用都打印goid = 0,足以证明Go调度器的调度由g0完成(如果你认为还是缺乏说服力,可以写复杂一些的demo):$ GODEBUG=schedtrace=1000 igo run demo1.goschedule(): goid = 0schedule(): goid = 0SCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0// 省略几百行启发比结论更重要,希望各位朋友在学习Go调度器的时候,能多一些自己的探索和研究,而不仅仅停留在看看别人文章之上。参考资料Installing Go from source如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019… ...

April 15, 2019 · 2 min · jiezi

Go调度器系列(3)图解调度原理

如果你已经阅读了前2篇文章:《调度起源》和《宏观看调度器》,你对G、P、M肯定已经不再陌生,我们这篇文章就介绍Go调度器的基本原理,本文总结了12个主要的场景,覆盖了以下内容:G的创建和分配。P的本地队列和全局队列的负载均衡。M如何寻找G。M如何从G1切换到G2。work stealing,M如何去偷G。为何需要自旋线程。G进行系统调用,如何保证P的其他G’可以被执行,而不是饿死。Go调度器的抢占。12场景提示:图在前,场景描述在后。上图中三角形、正方形、圆形分别代表了M、P、G,正方形连接的绿色长方形代表了P的本地队列。场景1:p1拥有g1,m1获取p1后开始运行g1,g1使用go func()创建了g2,为了局部性g2优先加入到p1的本地队列。场景2:g1运行完成后(函数:goexit),m上运行的goroutine切换为g0,g0负责调度时协程的切换(函数:schedule)。从p1的本地队列取g2,从g0切换到g2,并开始运行g2(函数:execute)。实现了线程m1的复用。场景3:假设每个p的本地队列只能存4个g。g2要创建了6个g,前4个g(g3, g4, g5, g6)已经加入p1的本地队列,p1本地队列满了。蓝色长方形代表全局队列。场景4:g2在创建g7的时候,发现p1的本地队列已满,需要执行负载均衡,把p1中本地队列中前一半的g,还有新创建的g转移到全局队列(实现中并不一定是新的g,如果g是g2之后就执行的,会被保存在本地队列,利用某个老的g替换新g加入全局队列),这些g被转移到全局队列时,会被打乱顺序。所以g3,g4,g7被转移到全局队列。场景5:g2创建g8时,p1的本地队列未满,所以g8会被加入到p1的本地队列。场景6:在创建g时,运行的g会尝试唤醒其他空闲的p和m执行。假定g2唤醒了m2,m2绑定了p2,并运行g0,但p2本地队列没有g,m2此时为自旋线程(没有G但为运行状态的线程,不断寻找g,后续场景会有介绍)。场景7:m2尝试从全局队列(GQ)取一批g放到p2的本地队列(函数:findrunnable)。m2从全局队列取的g数量符合下面的公式:n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))公式的含义是,至少从全局队列取1个g,但每次不要从全局队列移动太多的g到p本地队列,给其他p留点。这是从全局队列到P本地队列的负载均衡。假定我们场景中一共有4个P,所以m2只从能从全局队列取1个g(即g3)移动p2本地队列,然后完成从g0到g3的切换,运行g3。场景8:假设g2一直在m1上运行,经过2轮后,m2已经把g7、g4也挪到了p2的本地队列并完成运行,全局队列和p2的本地队列都空了,如上图左边。全局队列已经没有g,那m就要执行work stealing:从其他有g的p哪里偷取一半g过来,放到自己的P本地队列。p2从p1的本地队列尾部取一半的g,本例中一半则只有1个g8,放到p2的本地队列,情况如上图右边。场景9:p1本地队列g5、g6已经被其他m偷走并运行完成,当前m1和m2分别在运行g2和g8,m3和m4没有goroutine可以运行,m3和m4处于自旋状态,它们不断寻找goroutine。为什么要让m3和m4自旋,自旋本质是在运行,线程在运行却没有执行g,就变成了浪费CPU?销毁线程不是更好吗?可以节约CPU资源。创建和销毁CPU都是浪费时间的,我们希望当有新goroutine创建时,立刻能有m运行它,如果销毁再新建就增加了时延,降低了效率。当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有GOMAXPROCS个自旋的线程,多余的没事做线程会让他们休眠(见函数:notesleep())。场景10:假定当前除了m3和m4为自旋线程,还有m5和m6为自旋线程,g8创建了g9,g8进行了阻塞的系统调用,m2和p2立即解绑,p2会执行以下判断:如果p2本地队列有g、全局队列有g或有空闲的m,p2都会立马唤醒1个m和它绑定,否则p2则会加入到空闲P列表,等待m来获取可用的p。本场景中,p2本地队列有g,可以和其他自旋线程m5绑定。场景11:(无图场景)g8创建了g9,假如g8进行了非阻塞系统调用(CGO会是这种方式,见cgocall()),m2和p2会解绑,但m2会记住p,然后g8和m2进入系统调用状态。当g8和m2退出系统调用时,会尝试获取p2,如果无法获取,则获取空闲的p,如果依然没有,g8会被记为可运行状态,并加入到全局队列。场景12:(无图场景)Go调度在go1.12实现了抢占,应该更精确的称为请求式抢占,那是因为go调度器的抢占和OS的线程抢占比起来很柔和,不暴力,不会说线程时间片到了,或者更高优先级的任务到了,执行抢占调度。go的抢占调度柔和到只给goroutine发送1个抢占请求,至于goroutine何时停下来,那就管不到了。抢占请求需要满足2个条件中的1个:1)G进行系统调用超过20us,2)G运行超过10ms。调度器在启动的时候会启动一个单独的线程sysmon,它负责所有的监控工作,其中1项就是抢占,发现满足抢占条件的G时,就发出抢占请求。场景融合如果把上面所有的场景都融合起来,就能构成下面这幅图了,它从整体的角度描述了Go调度器各部分的关系。图的上半部分是G的创建、负债均衡和work stealing,下半部分是M不停寻找和执行G的迭代过程。如果你看这幅图还有些似懂非懂,建议赶紧开始看雨痕大神的Golang源码剖析,章节:并发调度。总结,Go调度器和OS调度器相比,是相当的轻量与简单了,但它已经足以撑起goroutine的调度工作了,并且让Go具有了原生(强大)并发的能力,这是伟大的。如果你记住的不多,你一定要记住这一点:Go调度本质是把大量的goroutine分配到少量线程上去执行,并利用多核并行,实现更强大的并发。下集预告下篇会是源码层面的内容了,关于源码分析的书籍、文章可以先看起来了,先剧透一篇图,希望阅读下篇文章赶紧关注本公众号。推荐阅读Go调度器系列(1)起源Go调度器系列(2)宏观看调度器参考资料在学习调度器的时候,看了很多文章,这里列一些重要的:The Go scheduler: https://morsmachine.dk/go-sch...Go's work-stealing scheduler: https://rakyll.org/scheduler/,中文翻译版: https://lingchao.xin/post/gos...Go夜读:golang 中 goroutine 的调度: https://reading.developerlear…Scheduling In Go : Part I、II、III: https://www.ardanlabs.com/blo…,中文翻译版: https://www.jianshu.com/p/cb6…雨痕大神的golang源码剖析: github.com/qyuhen/book也谈goroutine调度器: https://tonybai.com/2017/06/2...kavya的调度PPT: https://speakerdeck.com/kavya…抢占的设计提案,Proposal: Non-cooperative goroutine preemption: https://github.com/golang/pro…如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/04/04/golang-scheduler-3-principle-with-graph/

April 6, 2019 · 1 min · jiezi

Go调度器系列(2)宏观看调度器

上一篇文章《Go语言高阶:调度器系列(1)起源》,学goroutine调度器之前的一些背景知识,这篇文章则是为了对调度器有个宏观的认识,从宏观的3个角度,去看待和理解调度器是什么样子的,但仍然不涉及具体的调度原理。三个角度分别是:调度器的宏观组成调度器的生命周期GMP的可视化感受在开始前,先回忆下调度器相关的3个缩写:G: goroutine,每个G都代表1个goroutineM: 工作线程,是Go语言定义出来在用户层面描述系统线程的对象 ,每个M代表一个系统线程P: 处理器,它包含了运行Go代码的资源。3者的简要关系是P拥有G,M必须和一个P关联才能运行P拥有的G。调度器的功能《Go语言高阶:调度器系列(1)起源》中介绍了协程和线程的关系,协程需要运行在线程之上,线程由CPU进行调度。在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上。Go的调度器也是经过了多个版本的开发才是现在这个样子的,1.0版本发布了最初的、最简单的调度器,是G-M模型,存在4类问题1.1版本重新设计,修改为G-P-M模型,奠定当前调度器基本模样1.2版本加入了抢占式调度,防止协程不让出CPU导致其他G饿死在$GOROOT/src/runtime/proc.go的开头注释中包含了对Scheduler的重要注释,介绍Scheduler的设计曾拒绝过3种方案以及原因,本文不再介绍了,希望你不要忽略为数不多的官方介绍。Scheduler的宏观组成Tony Bai在《也谈goroutine调度器》中的这幅图,展示了goroutine调度器和系统调度器的关系,而不是把二者割裂开来,并且从宏观的角度展示了调度器的重要组成。自顶向下是调度器的4个部分:全局队列(Global Queue):存放等待运行的G。P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G’时,G’优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS个。M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。调度器的生命周期接下来我们从另外一个宏观角度——生命周期,认识调度器。所有的Go程序运行都会经过一个完整的调度器生命周期:从创建到结束。即使下面这段简单的代码:package mainimport “fmt”// main.mainfunc main() { fmt.Println(“Hello scheduler”)}也会经历如上图所示的过程:runtime创建最初的线程m0和goroutine g0,并把2者关联。调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。示例代码中的main函数是main.main,runtime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。G拥有栈,M根据G中的栈信息和调度信息设置运行环境M运行GG退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。调度器的生命周期几乎占满了一个Go程序的一生,runtime.main的goroutine执行之前都是为调度器做准备工作,runtime.main的goroutine运行,才是调度器的真正开始,直到runtime.main结束而结束。GMP的可视化感受上面的两个宏观角度,都是根据文档、代码整理出来,最后我们从可视化角度感受下调度器,有2种方式。方式1:go tool tracetrace记录了运行时的信息,能提供可视化的Web页面。简单测试代码:main函数创建trace,trace会运行在单独的goroutine中,然后main打印"Hello trace"退出。func main() { // 创建trace文件 f, err := os.Create(“trace.out”) if err != nil { panic(err) } defer f.Close() // 启动trace goroutine err = trace.Start(f) if err != nil { panic(err) } defer trace.Stop() // main fmt.Println(“Hello trace”)}运行程序和运行trace:➜ trace git:(master) ✗ go run trace1.goHello trace➜ trace git:(master) ✗ lstrace.out trace1.go➜ trace git:(master) ✗➜ trace git:(master) ✗ go tool trace trace.out2019/03/24 20:48:22 Parsing trace…2019/03/24 20:48:22 Splitting trace…2019/03/24 20:48:22 Opening browser. Trace viewer is listening on http://127.0.0.1:55984效果:从上至下分别是goroutine(G)、堆、线程(M)、Proc(P)的信息,从左到右是时间线。用鼠标点击颜色块,最下面会列出详细的信息。我们可以发现:runtime.main的goroutine是g1,这个编号应该永远都不变的,runtime.main是在g0之后创建的第一个goroutine。g1中调用了main.main,创建了trace goroutine g18。g1运行在P2上,g18运行在P0上。P1上实际上也有goroutine运行,可以看到短暂的竖线。go tool trace的资料并不多,如果感兴趣可阅读:https://making.pusher.com/go-… ,中文翻译是:https://mp.weixin.qq.com/s/nf… 。方式2:Debug trace示例代码:// main.mainfunc main() { for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println(“Hello scheduler”) }}编译和运行,运行过程会打印trace:➜ one_routine2 git:(master) ✗ go build .➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000 ./one_routine2结果:SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]SCHED 1001ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 2002ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 3004ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 4005ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 5013ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello scheduler看到这密密麻麻的文字就有点担心,不要愁!因为每行字段都是一样的,各字段含义如下:SCHED:调试信息输出标志字符串,代表本行是goroutine调度器的输出;0ms:即从程序启动到输出这行日志的时间;gomaxprocs: P的数量,本例有8个P;idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;threads: os threads/M的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;spinningthreads: 处于自旋状态的os thread数量;idlethread: 处于idle状态的os thread的数量;runqueue=0: Scheduler全局队列中G的数量;[0 0 0 0 0 0 0 0]: 分别为8个P的local queue中的G的数量。看第一行,含义是:刚启动时创建了8个P,其中5个空闲的P,共创建5个M,其中1个M处于自旋,没有M处于空闲,8个P的本地队列都没有G。再看个复杂版本的,加上scheddetail=1可以打印更详细的trace信息。命令:➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000,scheddetail=1 ./one_routine2结果:截图可能更代码匹配不起来,最初代码是for死循环,后面为了减少打印加了限制循环5次每次分别打印了每个P、M、G的信息,P的数量等于gomaxprocs,M的数量等于threads,主要看圈黄的地方:第1处:P1和M2进行了绑定。第2处:M2和P1进行了绑定,但M2上没有运行的G。第3处:代码中使用fmt进行打印,会进行系统调用,P1系统调用的次数很多,说明我们的用例函数基本在P1上运行。第4处和第5处:M0上运行了G1,G1的状态为3(系统调用),G进行系统调用时,M会和P解绑,但M会记住之前的P,所以M0仍然记绑定了P1,而P1称未绑定M。总结时刻这篇文章,从3个宏观的角度介绍了调度器,也许你依然不知道调度器的原理,心里感觉模模糊糊,没关系,一步一步走,通过这篇文章希望你了解了:Go调度器和OS调度器的关系Go调度器的生命周期/总体流程P的数量等于GOMAXPROCSM需要通过绑定的P获取G,然后执行G,不断重复这个过程示例代码本文所有示例代码都在Github,可通过阅读原文访问:golang_step_by_step/tree/master/scheduler参考资料Go程序的“一生”也谈goroutine调度器Debug trace, 当前调度器设计人Dmitry Vyukov的文章Go tool trace中文翻译Dave关于GODEBUG的介绍最近的感受是:自己懂是一个层次,能写出来需要抬升一个层次,给他人讲懂又需要抬升一个层次。希望朋友们有所收获。如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/03/26/golang-scheduler-2-macro-view/ ...

March 27, 2019 · 2 min · jiezi

Go语言高阶:调度器系列(1)起源

如果把语言比喻为武侠小说中的武功,如果只是会用,也就是达到四五层,如果用的熟练也就六七层,如果能见招拆招也得八九层,如果你出神入化,立于不败之地十层。如果你想真正掌握一门语言的,怎么也得八层以上,需要你深入了解这门语言方方面面的细节。希望以后对Go语言的掌握能有八九层,怎么能不懂调度器!?Google、百度、微信搜索了许多Go语言调度的文章,这些文章上来就讲调度器是什么样的,它由哪些组成,它的运作原理,搞的我只能从这些零散的文章中形成调度器的“概貌”,这是我想要的结果,但这还不够。学习不仅要知其然,还要知其所以然。学习之前,先学知识点的历史,再学知识,这样你就明白,为什么它是当下这个样子。所以,我打算写一个goroutine调度器的系列文章,从历史背景讲起,循序渐进,希望大家能对goroutine调度器有一个全面的认识。这篇文章介绍调度器相关的历史背景,请慢慢翻阅。远古时代上面这个大家伙是ENIAC,它诞生在宾夕法尼亚大学,是世界第一台真正的通用计算机,和现代的计算机相比,它是相当的“笨重”,它的计算能力,跟现代人手普及的智能手机相比,简直是一个天上一个地下,ENIAC在地下,智能手机在天上。它上面没有操作系统,更别提进程、线程和协程了。进程时代后来,现代化的计算机有了操作系统,每个程序都是一个进程,但是操作系统在一段时间只能运行一个进程,直到这个进程运行完,才能运行下一个进程,这个时期可以成为单进程时代——串行时代。和ENIAC相比,单进程是有了几万倍的提度,但依然是太慢了,比如进程要读数据阻塞了,CPU就在哪浪费着,伟大的程序员们就想了,不能浪费啊,怎么才能充分的利用CPU呢?后来操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了。线程时代多进程真实个好东西,有了对进程的调度能力之后,伟大的程序员又发现,进程拥有太多资源,在创建、切换和销毁的时候,都会占用很长的时间,CPU虽然利用起来了,但CPU有很大的一部分都被用来进行进程调度了,怎么才能提高CPU的利用率呢?大家希望能有一种轻量级的进程,调度不怎么花时间,这样CPU就有更多的时间用在执行任务上。后来,操作系统支持了线程,线程在进程里面,线程运行所需要资源比进程少多了,跟进程比起来,切换简直是“不算事”。一个进程可以有多个线程,CPU在执行调度的时候切换的是线程,如果下一个线程也是当前进程的,就只有线程切换,“很快”就能完成,如果下一个线程不是当前的进程,就需要切换进程,这就得费点时间了。这个时代,CPU的调度切换的是进程和线程。多线程看起来很美好,但实际多线程编程却像一坨屎,一是由于线程的设计本身有点复杂,而是由于需要考虑很多底层细节,比如锁和冲突检测。协程多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存(每个线程的内存占用级别为MB),线程多了之后调度也会消耗大量的CPU。伟大的程序员们有开始想了,如何才能充分利用CPU、内存等资源的情况下,实现更高的并发?既然线程的资源占用、调度在高并发的情况下,依然是比较大的,是否有一种东西,更加轻量?你可能知道:线程分为内核态线程和用户态线程,用户态线程需要绑定内核态线程,CPU并不能感知用户态线程的存在,它只知道它在运行1个线程,这个线程实际是内核态线程。用户态线程实际有个名字叫协程(co-routine),为了容易区分,我们使用协程指用户态线程,使用线程指内核态线程。协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。协程和线程有3种映射关系:N:1,N个协程绑定1个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点,1个进程的所有协程都绑定在1个线程上,一是某个程序用不了硬件的多核加速能力,二是一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。1:1,1个协程绑定1个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点,但有一个缺点是协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵了。M:N,M个协程绑定1个线程,是N:1和1:1类型的结合,克服了以上2种模型的缺点,但实现起来最为复杂。协程是个好东西,不少语言支持了协程,比如:Lua、Erlang、Java(C++即将支持),就算语言不支持,也有库支持协程,比如C语言的coroutine(风云大牛作品)、Kotlin的kotlinx.coroutines、Python的gevent。goroutineGo语言的诞生就是为了支持高并发,有2个支持高并发的模型:CSP和Actor。鉴于Occam和Erlang都选用了CSP(来自Go FAQ),并且效果不错,Go也选了CSP,但与前两者不同的是,Go把channel作为头等公民。就像前面说的多线程编程太不友好了,Go为了提供更容易使用的并发方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。Go中,协程被称为goroutine(Rob Pike说goroutine不是协程,因为他们并不完全相同),它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。Go语言的老调度器终于来到了Go语言的调度器环节。调度器的任务是在用户态完成goroutine的调度,而调度器的实现好坏,对并发实际有很大的影响,并且Go的调度器就是M:N类型的,实现起来也是最复杂。现在的Go语言调度器是2012年重新设计的(设计方案),在这之前的调度器称为老调度器,老调度器的实现不太好,存在性能问题,所以用了4年左右就被替换掉了,老调度器大概是下面这个样子:最下面是操作系统,中间是runtime,runtime在Go中很重要,许多程序运行时的工作都由runtime完成,调度器就是runtime的一部分,虚线圈出来的为调度器,它有两个重要组成:M,代表线程,它要运行goroutine。Global G Queue,是全局goroutine队列,所有的goroutine都保存在这个队列中,goroutine用G进行代表。M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。老调度器有4个缺点:创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M’。M中的mcache是用来存放小对象的,mcache和栈都和M关联造成了大量的内存开销和差的局部性。系统调用导致频繁的线程阻塞和取消阻塞操作增加了系统开销。Go语言的新调度器面对以上老调度的问题,Go设计了新的调度器,设计文稿:https://golang.org/s/go11sched新调度器引入了:P:Processor,它包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。work stealing:当M绑定的P没有可运行的G时,它可以从其他运行的M’那里偷取G。现在,调度器中3个重要的缩写你都接触到了,所有文章都用这几个缩写,请牢记:G: goroutineM: 工作线程P: 处理器,它包含了运行Go代码的资源,M必须和一个P关联才能运行G。这篇文章的目的不是介绍调度器的实现,而是调度器的一些理念,帮助你后面更好理解调度器的实现,所以我们回归到调度器设计思想上。调度器的有两大思想:复用线程:协程本身就是运行在一组线程之上,不需要频繁的创建、销毁线程,而是对线程的复用。在调度器中复用线程还有2个体现:1)work stealing,当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。2)hand off,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。利用并行:GOMAXPROCS设置P的数量,当GOMAXPROCS大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在多个CPU核上同时运行,使得并发利用并行。另外,GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。调度器的两小策略:抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。上面提到并行了,关于并发和并行再说一下:Go创始人Rob Pike一直在强调go是并发,不是并行,因为Go做的是在一段时间内完成几十万、甚至几百万的工作,而不是同一时间同时在做大量的工作。并发可以利用并行提高效率,调度器是有并行设计的。并行依赖多核技术,每个核上在某个时间只能执行一个线程,当我们的CPU有8个核时,我们能同时执行8个线程,这就是并行。结束语这篇文章的主要目的是为后面介绍Go语言调度器做铺垫,由远及近的方式简要介绍了多进程、多线程、协程、并发和并行有关的“史料”,希望你了解为什么Go采用了goroutine,又为何调度器如此重要。如果你等不急了,想了解Go调度器相关的原理,看下这些文章:设计方案:https://golang.org/s/go11sched代码中关于调度器的描述:https://golang.org/src/runtim…引用最多的调度器文章:https://morsmachine.dk/go-sch…kavya的PPT,目前看到的讲调度最好的PPT:https://speakerdeck.com/kavya…work stealing论文:http://supertech.csail.mit.ed…分析调度器的论文(就问你6不6,还有论文研究):http://www.cs.columbia.edu/~a…声明:关于老调度器的资料已经完全搜不到,根据新版调度器设计方案的描述,想象着写了老调度器这一章,可能存在错误。参考资料https://en.wikipedia.org/wiki…https://en.wikipedia.org/wiki...https://en.wikipedia.org/wiki...https://golang.org/doc/faq#go...https://golang.org/s/go11schedhttps://golang.org/src/runtim…如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/03/10/golang-scheduler-1-history

March 10, 2019 · 1 min · jiezi