本文共 1591 字,大约阅读时间需要 5 分钟。
起初,计算机在批处理模式下一个一个地执行任务。后来在上世纪60年代开发了多任务分时操作系统,在70年代 被广泛用于网络服务器、ftp、telnet、以及后来地httpd上,httpd使用fork
子进程的方式来处理每个网连接。
在分时系统上,系统记录当前执行进程的状态,然后在不同进程之间快速切换以便让CPU去执行每个进程,这样便达到了并发执行的效果。切换的过程称作上下文切换
上下文切换需要付出3个代价:
由于进程切换代价太大,由此出现了线程。线程的概念和进程差不多,不同的是线程之间共享内存空间,这样的话,相比之下,线程比进程切换轻量一些。
但是线程同样需要上下文切换,许多状态也需要保存和重新加载。
goroutine在线程的基础上又向前走了一步。
goroutines没有依赖操作系统内核去管理分时调度,而是依赖自身的合作调度(cooperatively scheduled)。Go runtime scheduler
会合理地调度goroutines,当goroutine出现一下情况时开始调度:
channel
的send和receive操作被阻塞。go
关键字创建新routine在Go运行时中,多个goroutine复用单个操作系统线程。这样,goroutine的创建和切换就非常轻便。正常情况下,一个进程下,可以有上万个goroutine,甚至上十万级。
从语言角度看,Go调度像是函数调用。编译器知道在用的寄存器并且会自动保存他们。
Go调度器使用一个线程时会保持一个goroutine 栈,但是返回时可能时一个不同的goroutine栈。这就好比在多线程程序里,一个线程可能会在任意时刻被抢占。
Go运行时将运行中的goroutine合理安排给一个空闲的线程,这样一个Go进程里就会有相对较少的线程。
上面讲到了goroutine如何减少了管理成千上万线程的开销,下面从栈管理上了解一下goroutine。
上图是一个典型的进程内存布局图。在进程的内存空间中,堆内存在下面,和代码区一样。栈分配在虚拟内存的上方,由上向下分配。
为了避免堆内存和栈“撞车“,操作系统会安排一块“guard page“的区域,这块区域不可访问。
线程之间公用进程的地址空间,每个线程也有自己的栈和“guard page“。
由于每个线程需要的栈大小不可预估,为了防止“撞到“gard page
,一般会把每个线程栈分配的很大,这样以来整个进程的可用地址空间就很小了。
每个goroutine开始执行的时候从堆中分配一块小的栈,Go1.5版本分配2k。
goroutine不再使用guard pages
,而是在函数执行之前先调用一个检测函数来确定是否有足够栈空间,如果有就正常执行。如果没有,Go运行时从堆中分配一块较大的栈,把当前执行的栈内容拷贝到新分配的栈中,然后重新执行函数。
这样的机制保证了goroutine的起始栈可以相对较小,然后随着需求来增加。所以说,在Go程序员来说goroutine相当轻便。
https://nullprogram.com/blog/2015/05/15/
转载地址:http://hmuwz.baihongyu.com/