原文链接:https://www.f2er.com/go/189308.html前言
Golang(Go)作为近几年兴起的语言,其本身的特点使其兼顾了性能与开发效率,加上学习的门槛比较低,很快便普及开来。众所周知,Go在处理并发上有着天生的优势,使用Go开发的UI层,支撑了多盟RTB日均五十亿时延要求在100ms内的广告请求。这篇文章就来简单介绍下这种处理并发的优势是如何实现的。
并发
Concurrency is a property of systems in which several computations are executing simultaneously,and potentially interacting with each other,维基百科上这样定义并发。多进程在同一个核内分时调度执行或者在多核下并行执行,都可以被称为并发。配合各种粒度的锁使用的多线程是我们最常用的并发模型,其它还有诸如函数式并发编程模型、Erlang中使用的Actor模型以及Golang中使用的CSP模型等。
进程,线程,协程
我们先来看看操作系统对并发的支持,在linux系统中,进程是对执行的程序的抽象,这层抽象主要用来描述执行代码镜像、虚拟内存空间、各种内核资源等等;而线程则是操作系统对最小执行单元的抽象,这层抽象主要描述cpu内各种寄存器等的状态。进程在是线程之上的构建的抽象屏障,是可执行代码和所操作数据的集合。
内核负责对进程、内核线程进行调度,系统会把不同的进程运行在各个cpu上来提升cpu的利用率,当一个进程阻塞时,cpu会被调度执行其他的进程。调度的目的是提高系统的在单位时间内执行的任务数,让进程占用的资源更加合理,系统对于进程和线程的调度是无差别的。其中,线程由于共享了虚拟内存空间等资源,线程在进行上下文切换时也不需要进行诸如保存、装载各种状态数据等资源,刷新TLB(x86)等操作,所以线程调度会比进程调度高效很多。
除了操作系统级别提供的线程和进程外,还有一种被称为Green Threads的用户态线程的概念。Green Threads可以理解为在应用程序级别实现了类似操作系统线程这样的概念,是在操作系统之上构建的并发对象。那么用户态线程可以用来做什么呢?最显而易见的是:即使是在不支持多进程(线程)的操作系统上,我们依然可以通过在语言层面上实现Green Threads来支持并发逻辑;其次是Green Threads的创建和调度都在用户空间里进行,不需要进行用户态与内核态的切换;最重要的是操作系统提供的线程是一个强大而复杂的对象,对于大多数简单的应用程序来说,过于“重”了,而Green Threads则可以被构造的非常简单,调度策略也实现的更加简单,占用更少的资源。虽然Green Threads不能自动实现被多个处理器调用,但可以通过实现达成这个目的,Go里的Goroutine就是一个很好的例子。
Goroutine
首先Goroutine通过调度,将多个用户态线程绑定在在若干内核线程上来实现,并使用了Work stealing调度算法来达到多核处理的效果。
如下图所示:
Go的runtime定义了M、P、G三个角色,分别代表POSIX Threads,Processor和Goroutine。P可以理解为执行上下文,也就是context,我们在Golang 1.5之前的版本需要设置的GOMAXPROCS数就是指P的数量。P负责完成对G和M的调度,我们可以把M理解为操作系统资源的抽象,是真正的执行体;把G理解为用户程序要执行的代码的抽象,是执行代码和数据的集合。P用执行体M来执行G,并且维护了一个deque来存放可执行的G,当前G执行结束,M就空闲了下来,P就可以从deque的顶部取出下一个G在M上继续执行。
那么如果G中执行了带阻塞的系统调用,调度会有什么样的变化呢?如下图所示:
当M去执行该系统调用时线程会阻塞并被操作系统挂起,这个时候P会把当前的G留在原来的M中处理,然后从deque里取出下一个G并创建一个新的M对象来执行它。被丢弃的G-M对完成系统调用变成可执行状态时,又会在合适的时机被重新调度执行。这也就是为什么即使GOMAXPROCS被设置成1,Goroutine还是能用到多核处理。
而当一个P对象将维护的deque里的G全部执行完之后,可以从别的P的deque底部拿到一半的G放入自己的deque中执行,这也就是为什么叫做Work stealing算法,这也是Goroutine为何高效的一个很大原因。
G比系统线程要简单许多,相对于在内核态进行context switch,G的切换代价低了很多,调度策略非常简单,毕竟操作系统要为各种复杂的场景提供完整的解决方案,而通常我们应用程序层面解决的问题都相对简单。
非阻塞IO与IO多路复用
现在我们知道协程的创建和上线文切换都非常“轻”,但是协程的阿喀琉斯之踵在于进行带阻塞系统调用时执行体M会被阻塞,这就需要创建新的系统资源,而在高并发的web场景下如果使用阻塞的IO调用,网络IO大概率阻塞较长的时间,导致我们还是要创建大量的系统线程,所以Go需要尽量使用非阻塞的系统调用,虽然Go的标准库提供的是同步阻塞的IO模型,但底层其实是使用内核提供的非阻塞的IO模型。当Goroutine进行IO操作而数据未就绪时,syscall返回error,当前执行的Goroutine被置为阻塞态而M并没有被阻塞,P就可以继续使用当前执行体M继续执行下一个G,这样就不需要再去创建新的M。
当然只有非阻塞IO还不够,Go抽象了netpoller对象来进行IO多路复用,在linux下通过epoll来实现IO多路复用。当G由于IO未就绪而被置为阻塞态时,netpoller将对应的文件描述符注册到epoll实例中进行epoll_wait,就绪的文件描述符回调通知给阻塞的G,G更新为就绪状态等待调度继续执行,这种实现使得Golang在进行高并发的网络通信时变得非常强大,相比于PHP-fpm的多进程模型,Golang Http Server使用很少的线程资源运行非常多的Goroutine,而且尽可能的让每一个线程都忙碌起来,而不是阻塞在IO调用上,提高了cpu的利用率。
最后
Golang依靠协程和底层的IO多路复用模型,让我们可以简单的通过同步编程的方式来解决高并发的IO密集型操作,语言本身工程性也非常强。Golang基本保持着每半年发布一个正式版本的迭代速度,相信随着基础库的完善、GC的优化等,未来还会有更大的潜力。
欢迎关注、讨论、交流、建议,长按二维码识别关注喔。
查看原文:http://www.zoues.com/2016/10/20/golang%e7%9a%84%e5%b9%b6%e5%8f%91%e5%a4%84%e7%90%86/