本文目录一览:
golang之context详解
为什么需要context
在go服务器中,对于每个请求的request都是在单独的goroutine中进行的,处理一个request也可能设计多个goroutine之间的交互, 使用context可以使开发者方便的在这些goroutine里传递request相关的数据、取消goroutine的signal或截止日期
在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。熟悉channel的朋友应该都见过使用done channel来处理此类问题。比如以下这个例子:
上述例子中定义了一个buffer为0的channel done, 子协程运行着定时任务。如果主协程需要在某个时刻发送消息通知子协程中断任务退出,那么就可以让子协程监听这个done channel,一旦主协程关闭done channel,那么子协程就可以推出了,这样就实现了主协程通知子协程的需求。这很好,但是这也是有限的。
如果我们可以在简单的通知上附加传递额外的信息来控制取消:为什么取消,或者有一个它必须要完成的最终期限,更或者有多个取消选项,我们需要根据额外的信息来判断选择执行哪个取消选项。
考虑下面这种情况:假如主协程中有多个任务1, 2, …m,主协程对这些任务有超时控制;而其中任务1又有多个子任务1, 2, …n,任务1对这些子任务也有自己的超时控制,那么这些子任务既要感知主协程的取消信号,也需要感知任务1的取消信号。
如果还是使用done channel的用法,我们需要定义两个done channel,子任务们需要同时监听这两个done channel。嗯,这样其实好像也还行哈。但是如果层级更深,如果这些子任务还有子任务,那么使用done channel的方式将会变得非常繁琐且混乱。
我们需要一种优雅的方案来实现这样一种机制:
上层任务取消后,所有的下层任务都会被取消;中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。
这个时候context就派上用场了。我们首先看看context的结构设计和实现原理。
context接口
先看Context接口结构,看起来非常简单。
}
Context接口包含四个方法:
Deadline返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false。
Done 当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil。
Err 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded。
Value 返回context存储的键值对中当前key对应的值,如果没有对应的key,则返回nil。
可以看到Done方法返回的channel正是用来传递结束信号以抢占并中断当前任务;Deadline方法指示一段时间后当前goroutine是否会被取消;以及一个Err方法,来解释goroutine被取消的原因;而Value则用于获取特定于当前任务树的额外信息。而context所包含的额外信息键值对是如何存储的呢?其实可以想象一颗树,树的每个节点可能携带一组键值对,如果当前节点上无法找到key所对应的值,就会向上去父节点里找,直到根节点。
emptyCtx
emptyCtx是一个int类型的变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为context树的根节点。
Background和TODO只是用于不同场景下: Background通常被用于主函数、初始化以及测试中,作为一个顶层的context,也就是说一般我们创建的context都是基于Background;而TODO是在不确定使用什么context的时候才会使用。
用法 :
怎么学习golang
已经有好多程序员都把Go语言描述为是一种所见即所得(WYSIWYG)的编程语言。这是说,代码要做的事和它在字面上表达的意思是完全一致的。 在这些新语言中,包含D,Go,Rust和Vala语言,Go曾一度出现在TIOBE的排行榜上面。与其他新语言相比,Go的魅力明显要大很多。Go的成熟特征会得到许多开发者的欣赏,而不仅仅是因为其夸大其词的曝光度。下面我们来一起探讨一下谷歌开发的Go语言以及谈谈Go为什么会吸引众多开发者: 快速简单的编译 Go编译速度很快,如此快速的编译使它很容易作为脚本语言使用。关于编译速度快主要有以下几个原因:首先,Go不使用头文件;其次如果一个模块是依赖A的,这反过来又取决于B,在A里面的需求改变只需重新编译原始模块和与A相依赖的地方;最后,对象模块里面包含了足够的依赖关系信息,所以编译器不需要重新创建文件。你只需要简单地编译主模块,项目中需要的其他部分就会自动编译,很酷,是不是? 通过返回数值列表来处理错误信息 目前,在本地语言里面处理错误的方式主要有两种:直接返回代码或者抛异常。这两种都不是最理想的处理方式。其中返回代码是非常令人沮丧的,因为返回的错误代码经常与从函数中返回的数据相冲突。Go允许函数返回多个值来解决这个问题。这个从函数里面返回的值,可以用来检查定义的类型是否正确并且可以随时随地对函数的返回值进行检查。如果你对错误值不关心,你可以不必检查。在这两种情况下,常规的返回值都是可用的。 简化的成分(优先于继承) 通过使用接口,类型是有资格成为对象中一员的,就像Java指定行为一样。例如在标准库里面的IO包,定义一个Writer来指定一个方法,一个Writer函数,其中输入参数是字节数组并且返回整数类型值或者错误类型。任何类型实现一个带有相同签名的Writer方法是对IO的完全实现,Writer接口。这种是解耦代码而不是优雅。它还简化了模拟对象来进行单元测试。例如你想在数据库对象中测试一个方法,在标准语言中,你通常需要创建一个数据库对象,并且需要进行大量的初始化和协议来模拟对象。在Go里面,如果该方法需要实现一个接口,你可以创建任何对该接口有用的对象,所以,你创建了MockDatabase,这是很小的对象,只实现了几个需要运行和模拟的接口——没有构造函数,没有附件功能,只是一些方法。 简化的并发性 相对于其他语言,并发性在Go里面显得更加容易。把‘go’关键字放在任意函数前面然后那个函数就会在其go-routine自动运行(一个很轻的线程)。go-routines是通过通道进行交流并且基本上封锁了所有的队列消息。普通工具对相互排斥是有用,但是Go通过使用通道来踢掉并发性任务和坐标更加容易。 优秀的错误消息 所有与Go相似的语言,自身作出的诊断都是无法与Go相媲美的。例如,一个死锁程序,在Go运行时会通知你目前哪个线程导致了这种死锁。编译的错误信息是非常详细全面和有用的。 其他 这里还有许多其他吸引人的地方,下面就一概而过的介绍一下,比如高阶函数、垃圾回收、哈希映射和可扩展的数组内置语言(部分语言语法,而不是作为一个库)等等。 当然,Go并不是完美无瑕。在工具方面还有些不成熟的地方和用户社区较小等,但是随着谷歌语言的不断发展,肯定会有整治措施出来。尽管许多语言,尤其是D、Rust和Vala旨在简化C++并且对其进行简化,但它们给人的感觉仍是“C++看上去要更好”。
【Go语言的优势】
可直接编译成机器码,不依赖其他库,glibc的版本有一定要求,部署就是扔一个文件上去就完成了。
静态类型语言,但是有动态语言的感觉,静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题,动态语言的感觉就是有很多的包可以使用,写起来的效率很高。
语言层面支持并发,这个就是Go最大的特色,天生的支持并发,我曾经说过一句话,天生的基因和整容是有区别的,大家一样美丽,但是你喜欢整容的还是天生基因的美丽呢?Go就是基因里面支持的并发,可以充分的利用多核,很容易的使用并发。
内置runtime,支持垃圾回收,这属于动态语言的特性之一吧,虽然目前来说GC不算完美,但是足以应付我们所能遇到的大多数情况,特别是Go1.1之后的GC。
简单易学,Go语言的作者都有C的基因,那么Go自然而然就有了C的基因,那么Go关键字是25个,但是表达能力很强大,几乎支持大多数你在其他语言见过的特性:继承、重载、对象等。
丰富的标准库,Go目前已经内置了大量的库,特别是网络库非常强大,我最爱的也是这部分。
内置强大的工具,Go语言里面内置了很多工具链,最好的应该是gofmt工具,自动化格式化代码,能够让团队review变得如此的简单,代码格式一模一样,想不一样都很困难。
跨编译,如果你写的Go代码不包含cgo,那么就可以做到window系统编译linux的应用,如何做到的呢?Go引用了plan9的代码,这就是不依赖系统的信息。
内嵌C支持,前面说了作者是C的作者,所以Go里面也可以直接包含c代码,利用现有的丰富的C库。
【golang详解】go语言GMP(GPM)原理和调度
Goroutine调度是一个很复杂的机制,下面尝试用简单的语言描述一下Goroutine调度机制,想要对其有更深入的了解可以去研读一下源码。
首先介绍一下GMP什么意思:
G ———– goroutine: 即Go协程,每个go关键字都会创建一个协程。
M ———- thread内核级线程,所有的G都要放在M上才能运行。
P ———– processor处理器,调度G到M上,其维护了一个队列,存储了所有需要它来调度的G。
Goroutine 调度器P和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行
模型图:
避免频繁的创建、销毁线程,而是对线程的复用。
1)work stealing机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
2)hand off机制
当本线程M0因为G0进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU。M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:
如果有空闲的P,则获取一个P,继续执行G0。
如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。
如下图
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行
在Go中一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死。
具体可以去看另一篇文章
【Golang详解】go语言调度机制 抢占式调度
当创建一个新的G之后优先加入本地队列,如果本地队列满了,会将本地队列的G移动到全局队列里面,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。
协程经历过程
我们创建一个协程 go func()经历过程如下图:
说明:
这里有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;处理器本地队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务。
G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;
一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G
上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。
work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行,如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。
如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为。
Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:
用户态阻塞/唤醒
当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为,尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine。), 然后再是P的本地队列和全局队列。
系统调用阻塞
当M执行某一个G时候如果发生了阻塞操作,M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P。当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。
队列轮转
可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。
M0
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了
G0
G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0
一个G由于调度被中断,此后如何恢复?
中断的时候将寄存器里的栈信息,保存到自己的G对象里面。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。
我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关GMP的底层原理可以去看Go调度器 G-P-M 模型的设计者的文档或直接看源码
参考: ()
()
原创文章,作者:JKOC,如若转载,请注明出处:https://www.506064.com/n/137237.html