【回顾上集内容】
之前的文章《想要做到高并发和高性能,请先真正的理解它们,以及跟CPU,内存,分布式又有什么关系呢》有讲到进程、线程、协程、队列对内存需求的差别,然后在系统中各自可以维持的并发数对比数据。
也一笔带过对应的几种运行模式。
我们先来做一个假设,单个进程内存占用10M,单个线程内存占用2M,单个协程内存占用20K,队列任务内存占用2K,我们下面来看看内存与并发量的关系。
(具体的内存占用大小在不同的应用场景中会有很大的不同,所以这里只是为了方便计算而做的一个假设)
内存量 进程数 线程数 协程 队列任务
1G 100 500 50K 500K
2G 200 1000 100K 1000K
4G 400 2000 200K 2000K
8G 800 4000 400K 4000K
对应的几种运行模式
多进程: php fast-cgi
多线程: java web
协程: go
队列: nginx
这一次,我们再来详细的讲解进程、线程、协程、队列,以及这几种运行模式的特点和优势劣势对比。
【进程介绍】
每一个应用运行起来都会有自己的进程,因为进程是系统资源分配的基本单位。
在线程出现之前,进程也是CPU调度的基本单位。
每一个进程创建出来,都会分配三种基本的内存资源,分别是代码段、数据段和堆栈段。
代码段和数据段分别保存着应用的执行代码和全局变量、常量、静态变量,这些就是不会变化或者很少变化的内容,当然内存占用相对也会比较少。
而应用运行起来,需要的更多资源就会在堆栈中用到。
其中堆空间是存放各种变量数据的地方,内存大小也是可以动态调整的。
而栈空间是子任务(线程、协程)独立存放自己的数据地方,比如:函数调用、参数、返回值和局部变量。
这样一来,子任务(线程、协程)之间就可以独立运行,而且还可以共享堆空间中的变量数据。
【线程介绍】
线程在新的操作系统中,也称为轻量级进程,因为现在的线程已经是CPU调度的基本单位了。
操作系统不仅仅维持一个进程表,而且还会维持一个线程表,这样操作系统就可以把线程作为调度单位。
线程是进程内创建,可以共享进程的资源,所以,线程自身独立的资源依赖就会少很多,因为只需要为每个线程分配独立的栈空间。
而线程的栈空间是固定大小的,如果程序比较复杂,或者里面的数据量大,为了不出现“栈空间不足”的错误,就必须把栈空间设置的足够大才行。
于是,线程是固定的栈空间S(足够大),总共运行多少线程T,占用总的栈空间就可以简单计算出来=T*S。
这个资源占用量相对T个进程来说,还是少了很多的,毕竟线程是共享了进程的代码段、数据段和堆空间。
【协程介绍】
协程是可以在应用态协作的程序,它的调度不是操作系统处理,而是应用系统自己来调度处理,也称为轻量级线程。
在操作系统可以独立调度线程之前,在线程还是作为应用的程序包,有应用程序自己调度和管理的时候,其实那种线程也就跟现在的协程是一个概念了。
所以,这里我们就不再讲以前的那种应用内的线程,只讲新的协程。
如果说到线程,就是新的可以被操作系统独立调度的线程。
协程作为应用系统内调度的子任务单元,当然也是会共享进程的各种资源,除了自己的栈空间(函数调用、参数、返回值、局部变量)。
而协程与线程主要的区别有两个,最大的就是调度方式,线程是操作系统调度,协程是应用系统自己调度。
另外一个区别,协程的栈空间是可以动态调整的,这样空间利用率就可以更高,一个任务需要2K空间就分配2K内存,一个任务需要20M空间就分配20M,而不用担心栈空间不够或者空间浪费。
由于上面的两个原因,协程的优势也就凸显出来。
1 协程可以更好的利用CPU,不用把CPU浪费在线程调度和上下文切换上。
2 协程可以更好的利用内存,不用全都分配一个偏大的空间,只需要分配需要的对应空间即可。
【队列介绍】
这里的队列不是独立的消息队列服务,而只是应用中维持数据的一个队列,很多时候会是一个数组或者链表。
队列里面保存的也不是一个子任务,而只是一个数据,具体这个数据拿出来之后要启动什么子任务,这个队列是不关心的。
队列只是一个缓冲带,把更多的独立数据先临时保持住,应用系统有多大的能力消化吸收就从里面用多快的速度进行处理。
从上面可以看出,队列比协程还要简单,都没有所谓各自独立的子任务,也就没有了独立的栈空间。
所以,这样的简化,也就带来了更少的资源开销,更少的任务调度。
接下来,我们结合实际中的几种运行模式来介绍下现状和发展。
【多进程:php fast-cgi】
php在使用fast-cgi之前,更多是多线程模式,为什么转而回到多进程模式呢?
多线程模式是为每个网络请求创建一个线程来处理这个请求,当请求执行结束,再销毁这个线程。
于是,当网站的请求量高的时候,意味着反复的为这些请求创建和销毁线程,这个开销就变得比较大,效率也就下降了。
在多进程模式下,进程是复用的,不会反复的创建和销毁,所以就没有之前多线程模式那样大的资源浪费了。
当然,多进程的问题就像上面说到的,内存开销大,系统调度开销大,所以也就意味着并发量相对就会比较小。
所以,新的php swoole框架也把协程引入进来,同时把多路复用的epoll网络模型引入进来,这样就带来了很明显的好处。
1 协程占用内存小,可以同时维持更多的并发请求。
2 epoll网络模型非阻塞而且系统开销少,可以更好的利用CPU资源,同时避免了网络IO阻塞影响整体的任务执行。
【多线程:java web】
java多线程的运行模式用到线程池的技术,并不是每个请求都会启动一个线程来处理,而是复用线程池中的线程,这样也就类似上面php fast-cgi模式,很好的避免了线程频繁创建和销毁所带来的损耗。
线程比进程更轻量,所以单个线程的内存占用会比单个进程少,但是因为线程栈空间固定,在一些个别请求中,数据量很大,也可能会不得已要设置较大的栈空间,这样一来,内存浪费也是会比较严重了。
在之前的文章《认识IO的问题才能更好的设计和开发出高并发和高性能的系统》,也有提到,java中更好支持IO密集型的框架,可以用netty,同样是支持多路复用的epoll模型,也简化了自己去实现NIO的过程。
kotlin.corouties 了解一下,简化的JAVA,1.3版本会发布协程的正式线上支持。
【协程:go】
go原生的支持协程,并且有完善的协程调度器,让协程在开发和运行时变得更加简单和高效。
作为新的开发语言,普及还需要时间,在网络编程的系统中,还是非常有竞争力的。
一步到位的支持高并发和高性能,说的太多就怕它骄傲了(站在巨人肩膀上,新思维、新技术)。
【队列:nginx】
nginx实际是一个master+多个worker,也算是多进程模式。但是work是单线程的,却可以支持超高的网络并发量,这就是nginx内部实际就是一个网络事件队列。
每个请求进来都是一个connection,然后这个connection就通过epoll_ctl注册到系统的网络IO事件中,当connection的网络事件准备好了才通过回调函数放到已就绪队列中。
而nginx就是epoll_wait不断的轮询这个就绪队列,然后再处理这里的事件。
网络请求的处理又有很多的阶段,每个阶段又可以有多个nginx模块来处理,这些nginx模块就是各个真正的任务处理系统。
nginx除了反向代理以及作为静态WEB服务器,也可以作为应用服务器,比如利用ngx_lua模块,就可以对WEB请求做实时动态的处理,来完成一个动态服务。
这样一来,nginx把网络请求放到事件队列中,ngx_lua利用协程把各个请求动态执行,也就可以高效的达到一个应用服务器的效果了,而且并发、性能也非常好。
【总结】
从上面几种模式中,我们都看到协程在新的框架、模块中用的越来越多,而且也确实能非常明显的提高系统的并发量。
而协程在20年前就已经提出来和运用,为什么到这几年才开始普及和应用开来呢?
这是一个发展的问题:
1 以前多核并行运算少,网络编程没有这么普遍(硬件);
2 以前留下来的代码库都不支持协程,重新开发难度大(软件);
3 以前的程序员大部分都不知道协程,自然支持的也少(人)。
同样的,多路复用epoll网络模型也在越来越多的系统中被使用,非阻塞带来高效的同时,还可以同步方式编码,所以,现在的程序员技术库武器越来越强大,开发出来的系统自然也不会太弱了。
在实战课程 《PHP秒杀系统 高并发高性能的极致挑战》中,也是针对这类高并发的业务场景做了特定的性能优化以及分布式方案,大家可以参考学习。