前言
在了解go(golang)语言之前,需要先复习(预习)下计算机的基础.这个在大学期间,计算机操作系统里面应该学过. 操作系统课程里面应该只是了解到了进程和线程的相关知识.对于实际场景中,并发需求经常出现, 对于java语言来说, 可以通过多线程并发来实现, 对于php来说,可以通过Swoole扩展来实现. 而go语言则从语言层面支持了协程实现, 并发编程这部分在最后再简单描述下.
一、进程
我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
1、组成
进程一般由程序、数据集合和进程控制块三部分组成。
1、程序用于描述进程要完成的功能,是控制进程执行的指令集;
2、数据集合是程序在执行时所需要的数据和工作区;
3、程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
2、特征
1)、动态性:进程是程序的一次执行过程,是临时、有生命期的,是动态产生,动态消亡的;
2)、并发性:任何进程都可以同其他进程一起并发执行;
3)、独立性:进程是系统进行资源分配和调度的一个独立单位;
4)、结构性:进程由程序、数据和进程控制块三部分组成。
3、状态
进程创建和销毁目前状态模型有三态、五态,这个可以参考下图, 具体在此不做赘述.
进程是抢占式的争夺CPU运行自身,而CPU单核的情况下同一时间只能执行一个进程的代码,但是多进程的实现则是通过CPU飞快的切换不同进程,因此使得看上去就像是多个进程在同时进行.
通信问题: 由于进程间是隔离的,各自拥有自己的内存内存资源, 因此相对于线程比较安全, 所以不同进程之间的数据只能通过 IPC(Inter-Process Communication) 进行通信共享.
二、线程
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
通信问题: 进程相当于一个容器,而线程而是运行在容器里面的,因此对于容器内的东西,线程是共同享有的,因此线程间的通信可以直接通过全局变量进行通信,但是由此带来的例如多个线程读写同一个地址变量的时候则将带来不可预期的后果,因此这时候引入了各种锁的作用,例如互斥锁等。
三、上下文切换
线程和进程的上下文切换
进程切换分3步:
1、切换页目录以使用新的地址空间
2、切换内核栈
3、切换硬件上下文
而线程切换只需要第2、3步,因此进程的切换代价比较大
三、协程
协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。
因为是自主开辟的异步任务,所以很多人也更喜欢叫它们纤程(Fiber),或者绿色线程(GreenThread)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
1、协程的目的
在传统的J2EE系统中都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,因为这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底。
最常见的例子就是JDBC(它是同步阻塞的),这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。而另外过多的线程,也会带来更多的ContextSwitch开销。
对于上述问题,现阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其代表派是node.js以及Java里的新秀Vert.x。
而协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销。
2、协程的特点
1、线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
2、线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
3、由于在同一个线程上,因此可以避免竞争关系而使用锁。
4、适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。
3、协程的原理
当出现IO阻塞的时候,由协程的调度器进行调度,通过将数据流立刻yield掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。比如Golang里的 go关键字其实就是负责开启一个Fiber,让func逻辑跑在上面。
由于协程的暂停完全由程序控制,发生在用户态上;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上。
因此,协程的开销远远小于线程的开销,也就没有了ContextSwitch上的开销。
四、附: 并发编程常见实现
1、多进程。多进程是在操作系统层面进行并发的基本模式,同时也是开销最大的模式。在 Linux 平台上,很多工具正是采用这种模式在工作,比如 PHP-FPM,它会有专门的主进程负责网络端口的监听和连接管理,还会有多个工作进程负责具体的请求处理。这种方法的好处在于简单、进程间互不影响,坏处在于系统开销大,因为所有的进程都是由内核管理的,而且不同进程的数据也是相互隔离的。
2、多线程。多线程在大部分操作系统上都属于系统层面的并发模式,也是我们使用最多的最有效的一种模式。目前,常见的几乎所有工具都会使用这种模式,线程比进程轻量级,线程间可以共享数据,开销要比多进程小很多,但是依旧比较大,且在高并发模式下,效率会有影响,比如 C10K 问题,即支持 1 万个并发连接需要一万个线程,这不但对系统资源有较高的要求,还对 CPU 管理这些线程带来巨大负担。
3、基于回调的非阻塞/异步 IO。为了解决 C10K 问题,在很多高并发服务器开发实践中,都会通过事件驱动的方式使用异步 IO,在这种模式下,一个线程可以维护多个 Socket 连接,从而降低系统开销,保持服务器的持续运转,它目前在 Node.js 中得到了很好的实践,实际上 Nginx 也使用了这种方式。但是使用这种模式,编程比多线程要复杂,通常需要借助 Linux 底层的库函数来实现。
4、协程。协程(Coroutine)本质上是一种用户态线程,你可以把它看作轻量级的线程,不需要操作系统来进行抢占式调度,系统开销极小,可以有效提高线程的任务并发性,避免多线程的缺点。使用协程的优点是编程简单,结构清晰;缺点是需要语言级别的支持,如果不支持,则需要用户在程序中自行实现相应的调度器。目前,原生支持协程的语言还很少,Go 语言就是其中这一,Go 语言中的协程称作「goroutine」,并且使用语言名称本身 go 做为协程的关键字,足见其在 Go 语言中的举足轻重。PHP 的 Swoole 扩展也是参考了 Go 协程的实现将其搬到 PHP 中。
有疑问加站长微信联系(非本文作者)