Go之goroutine

0 阅读8分钟

707f37a78a6c8233f7cd2f596f246ff.jpg

go语句意味着一个函数或方法的并发执行.go语句是由关键字和表达式组成的.简单说.表达式就是用于描述针对若干操作数的计算方法的式子.Go的表达式有很多种.其中就包括调用表达式.调用表达式所表达的是针对函数或方法的调用.其中的函数可以是命名的.也可以是匿名的.能够称为表达式语句的表达式.是创建go语句时唯一合法的表达式.如下函数不能被称为表达式语句:append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof和unsafe.Sizeof.前八个函数是Go语言创建的内建函数.最后三个则是标准库代码包unsafe中的函数.

示例:

func main() {
    go func() {
       fmt.Println("hello,go关键字")
    }()
}

注意:无论是否需要传递值给匿名函数.都不要忘了最后的那对圆括号.它们代表了对函数的调用行为.也是调用表达式的必要组成部分.在go关键字后面的调用表达式是不能用圆括号括起来的.

执行结果:

没有出现期望的hello,go关键字.

Go运行时系统对go语句中的函数(以下简称go函数)的执行是并发的.当go语句执行的时候.其中的go函数会被单独放入一个goroutine中.在这之后.该go函数的执行会独立于当前goroutine运行.一般情况下.位于go语句后面的那些语句并不会等到前者的go函数执行完成才开始执行.甚至在该go函数真正执行之前.运行时系统可能就已经开始执行后面的语句了.go函数并发执行.但谁先后不确定.

Go语言中有很多方法可以干预G的执行顺序.最简单的一种方法就是用time包中的sleep函数.

示例如下:

func main() {
    go func() {
       fmt.Println("hello,go关键字")
    }()
    time.Sleep(time.Second)
}

执行结果:

函数time.Sleep的作用是让调用它的goroutine暂停(进入Gwaiting状态)一段时间.

示例:

func main() {
    name := "one"
    go func() {
       fmt.Printf("Hello, %s!\n", name)
    }()
    name = "two"
    time.Sleep(time.Millisecond)
}

执行结果:

这进一步说明了执行的并发性.当go函数开始执行的时候.name="two"语句已经执行了.

主goroutine的运作:

封装main函数的goroutine称为主goroutine.主goroutine会由runtime.m0负责运行.

主goroutine所做的事情并不是执行main函数那么简单.它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸.在32位的计算机系统中此最大尺寸为250MB.在64位计算机系统此尺寸大小为1GB.如果某个goroutine的栈空间大小尺寸大于这个限制/那么运行时系统就会发出一个栈溢出的运行时恐慌.随即.这个Go程序的运行也会终止.

设定好goroutine的最大尺寸之后.主goroutine会在当前M的g0上执行系统监测任务.已知.系统监测任务的作用就是为了调度器查缺补漏.这也是监测任务的执行先于main函数的原因之一.

此后.主goroutine会进行一系列的初始化工作.涉及的工作内容大致如下.

1).检查当前M是否是runtime.m0.如果不是.就说明之前程序出现了某种问题.这时.主goroutine会立即抛出异常.这也意味着Go程序启动的失败.

2).创建一个特殊的defer语句.用于在主goroutine退出时做必要的善后处理.因为主goroutine也可能非正常的结束.这一点很有必要.

3).启用专用于在后台清扫内存垃圾的goroutine.并设置GC可用的标识.

4).执行main包中的init函数.

如果上述工作初始化成功完成.那么主goroutine就会去执行mian函数.在执行完main函数后.它还会检查主goroutine是否引起了运行时恐慌.并进行必要的处理.最后主goroutine会结束自己以及当前进程的运行.

在main函数执行期间.运行时系统会根据Go程序中的go语句.复用或新建goroutine来封装go函数.这些goroutine都会放入相应P的可运行G队列中.然后等待调度器的调度.这样的等待时间通常会非常短暂.但是有时如此短的时间也不容忽视.就像前面例子一样.它可能会使goroutine错过甚至永远失去运行时机.

runtime包与goroutine:

Go的标准库代码包runtime中的程序实体.提供了各种可以使用户程序与Go运行时系统交互的功能.如下:

1).runtime.GOMAXPROCS函数:

通过调用runtime.GOMAXPROCS函数.用户程序可以在运行期间.设置常规运行时系统中的P的最大数量.因为这样会引起"Stop the world".所以建议应用程序尽量早的并且更好的方式设置环境变量GOMAXPROCS.

Go运行时系统中的P最大数量范围总会是1~256.

2).runtime.Goexit函数:

调用runtime.Goexit函数之后.会立即使当前goroutine的运行终止.而其他goroutine并不会受此影响.runtime.Goexit函数在终止当前goroutine之前.会先执行该goroutine中所有还未执行的defer语句.

该函数会把终止的goroutine置于Gdead状态.并将其放入本地P的自由G列表.然后触发调度器的一轮调度流程.

3).runtime.Gosched函数:

runtime.Gosched函数的作用是暂停当前goroutine的运行.当前goroutine会被置为Grunnable状态.并放入调度器的可运行G队列.这也是使用"暂停"这个关键字的原因.经过调度器的调度.该goroutine马上会再次运行.

4).runtime.NumGoroutine函数:

runtime.NumGoroutine函数在被调用后.会返回当前Go运行时系统中处于非Gdead状态用户G的数量.这些goroutine被视为活跃的或者可被调度运行的.该函数的返回值总会大于等于1.

5).runtime.LockOSThread函数和runtime.UnLockOSThread函数:

对前者的调用会使当前goroutine与当前M锁定在一起.而对后者的调用则会解除这样的锁定.多次调用前者不会造成任何问题.但是只有最后一次调用会生效.可以向象成对同一个变量的多次赋值.另一方面.即使在之前没有调用过前者.对后者调用也不会产生任何副作用.

6).runtime/debug.SetMaxStack函数:

这个函数的功能是约束单个goroutine所能申请栈空间的最大尺寸.已知.在mian函数及init函数真正执行之前.主goroutine会对此数值进行默认设置.250MB和1GB分别是在32位和64位计算机系统的默认值.

该函数接收一个int类型的参数.该参数的含义是欲设定的栈空间的最大字节数.该函数在执行完毕的时候.会把之前设定的结果返回.

如果运行时系统在为某个goroutine增加栈空间的时候.发现它的尺寸超过了设定值.就会发起一个运行时恐慌并终止程序运行.

7).runtime/debug.SetMaxThreads:

runtime/debug.SetMaxThreads函数的作用是对Go运行时系统所使用的内核线程的数量(也可以认为是M的数量)进行设置.在引导程序中.该数量被设置成了1000.这对于操作系统和Go程序来说.都已经是一个足够大的值了.

该函数接受一个int类型的值.也会返回一个int类型的值.前者代表欲设定的新值.而后者代表之前设定的旧值.如果调用此函数设定的新值比运行时系统当前正在使用的M的数量还要小的话.就会引发一个运行时恐慌.另一方面.在对此函数的调用完成后.设定的新值就会立即发挥作用.每当运行时系统新建一个M.就会检查它当前所持M的数量.如果该数量大于M最大数量的设定.运行时系统就会发起一个同样的运行时恐慌.

8).runtime/debug.SetGCPercent runtime.GC  runtime/debug.FreeOSMemory:

前者用于设定触发自动GC的条件.后两者则用于手动触发GC.注意.自动GC在默认情况下是并发运行的.而手动GC则总是串行运行的.这也意味着.在后两个函数的执行期间.调度是停止的.runtime/debug.FreeOSMemory函数比runtime.GC函数多做了一件事.就是在GC之后还要清扫一次堆内存.

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

拔灯书尽红笺也.依旧无聊.玉漏迢迢.梦里寒花隔玉萧.

几竿修竹三更雨.叶叶萧萧.分付秋潮.莫误双鱼到谢桥. 纳兰.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路