如何理解高性能服务器的高性能、高并发?之二_风闻
蓝海大脑GPU服务器-水冷服务器、大数据一体机、图数据一体机01-12 18:59
高性能服务器到底是如何实现的?
当你在阅读文章的时候,有没有想过,服务器是怎么把这篇文章发送给你的呢?说起来很简单不就是一个用户请求吗?服务器根据请求从数据库中捞出这篇文章,然后通过网络发回去吗。其实有点复杂服务器端到底是如何并行处理成千上万个用户请求的呢?这里面又涉及到哪些技术呢?一、多进程历史上最早出现也是最简单的一种并行处理多个请求的方法就是利用多进程。比如在Linux世界中,可以使用fork、exec等系统调用创建多个进程,可以在父进程中接收用户的连接请求,然后创建子进程去处理用户请求。
1、多进程并行处理的优点
1)编程简单,非常容易理解;
2)由于各个进程的地址空间是相互隔离的,因此一个进程崩溃后并不会影响其它进程;
3)充分利用多核资源。
2、多进程并行处理的缺点1)各个进程地址空间相互隔离,这一优点也会变成缺点,那就是进程间要想通信就会变得比较困难,你需要借助进程间通信机制,想一想你现在知道哪些进程间通信机制,然后让你用代码实现呢?显然,进程间通信编程相对复杂,而且性能也是一大问题;
2)创建进程开销是比线程要大的,频繁的创建销毁进程无疑会加重系统负担。
二、多线程由于线程共享进程地址空间,因此线程间通信天然不需要借助任何通信机制,直接读取内存就好了。线程创建销毁的开销也变小了,要知道线程就像寄居蟹一样,房子(地址空间)都是进程的,自己只是一个租客,因此非常的轻量级,创建销毁的开销也非常小。我们可以为每个请求创建一个线程,即使一个线程因执行I/O操作——比如读取数据库等——被阻塞暂停运行也不会影响到其它线程。
由于线程共享进程地址空间,这在为线程间通信带来便利的同时也带来了无尽的麻烦。正是由于线程间共享地址空间,因此一个线程崩溃会导致整个进程崩溃退出,同时线程间通信简直太简单了,简单到线程间通信只需要直接读取内存就可以了,也简单到出现问题也极其容易,死锁、线程间的同步互斥、等等,这些极容易产生bug,无数程序员宝贵的时间就有相当一部分用来解决多线程带来的无尽问题。
虽然线程也有缺点,但是相比多进程来说,线程更有优势,但想单纯的利用多线程就能解决高并发问题也是不切实际的。因为虽然线程创建开销相比进程小,但依然也是有开销的,对于动辄数万数十万的链接的高并发服务器来说,创建数万个线程会有性能问题,这包括内存占用、线程间切换,也就是调度的开销。三、事件驱动:Event Loop到目前为止,提到“并行”二字就会想到进程、线程。但是并行编程只能依赖这两项技术吗?并不是这样的!还有另一项并行技术广泛应用在GUI编程以及服务器编程中,这就是近几年非常流行的事件驱动编程:event-based concurrency。大家不要觉得这是一项很难懂的技术,实际上事件驱动编程原理上非常简单。这一技术需要两种原料:1)event;
2)处理event的函数,这一函数通常被称为event handler;
由于对于网络通信服务器来说,处理一个用户请求时大部分时间其实都用在了I/O操作上,像数据库读写、文件读写、网络读写等。当一个请求到来,简单处理之后可能就需要查询数据库等I/O操作,我们知道I/O是非常慢的,当发起I/O后我们大可以不用等待该I/O操作完成就可以继续处理接下来的用户请求。所以一个event loop可以同时处理多个请求。
四、事件来源:IO多路复用IO多路复用技术通过一次监控多个文件描述,当某个“文件”(实际可能是im网络通信中socket)可读或者可写的时候我们就能同时处理多个文件描述符啦。这样IO多路复用技术就成了event loop的原材料供应商,源源不断的给我们提供各种event,这样关于event来源的问题就解决了。
五、问题:阻塞式IO当我们进行IO操作,比如读取文件时,如果文件没有读取完成,那么我们的程序(线程)会被阻塞而暂停执行,这在多线程中不是问题,因为操作系统还可以调度其它线程。但是在单线程的event loop中是有问题的,原因就在于当我们在event loop中执行阻塞式IO操作时整个线程(event loop)会被暂停运行,这时操作系统将没有其它线程可以调度,因为系统中只有一个event loop在处理用户请求,这样当event loop线程被阻塞暂停运行时所有用户请求都没有办法被处理。你能想象当服务器在处理其它用户请求读取数据库导致你的请求被暂停吗?
因此:在基于事件驱动编程时有一条注意事项,那就是不允许发起阻塞式IO。有的同学可能会问,如果不能发起阻塞式IO的话,那么该怎样进行IO操作呢?六、解决方法:非阻塞式IO为克服阻塞式IO所带来的问题,现代操作系统开始提供一种新的发起IO请求的方法,这种方法就是异步IO。对应的,阻塞式IO就是同步IO,关于同步和异步详见上文。异步IO时,假设调用aio_read函数(具体的异步IO API请参考具体的操作系统平台),也就是异步读取,当我们调用该函数后可以立即返回,并继续其它事情,虽然此时该文件可能还没有被读取,这样就不会阻塞调用线程了。此外,操作系统还会提供其它方法供调用线程来检测IO操作是否完成。七、基于事件驱动并行编程的难点虽然有异步IO来解决event loop可能被阻塞的问题,但是基于事件编程依然是困难的。
首先event loop是运行在一个线程中的,显然一个线程是没有办法充分利用多核资源的,有的同学可能会说那就创建多个event loop实例不就可以了,这样就有多个event loop线程了,但是这样一来多线程问题又会出现。
其次在于编程方面,异步编程需要结合回调函数(这种编程方式需要把处理逻辑分为两部分:一部分调用方自己处理,另一部分在回调函数中处理),这一编程方式的改变加重了程序员在理解上的负担,基于事件编程的项目后期会很难扩展以及维护。八、更好的方法有没有一种方法既能结合同步IO的简单理解又不会因同步调用导致线程被阻塞呢?答案是肯定的,这就是用户态线程(user level thread),也就是大名鼎鼎的协程。
虽然基于事件编程有这样那样的缺点,但是在当今的高性能高并发服务器上基于事件编程方式依然非常流行,但已经不是纯粹的基于单一线程的事件驱动了,而是 event loop + multi thread + user level thread。进程、线程、协程
一、什么是进程?
1、基本常识
计算机的核心是CPU,它承担了所有的计算任务;操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序、数据集合和进程控制块三部分组成:程序用于描述进程要完成的功能,是控制进程执行的指令集;
数据集合是程序在执行时所需要的数据和工作区;
程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
进程的特点:动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
并发性:任何进程都可以同其他进程一起并发执行;
独立性:进程是系统进行资源分配和调度的一个独立单位;
结构性:进程由程序、数据和进程控制块三部分组成。
**2、为什么要有多进程?**多进程目的是提高cpu的使用率。假设只有一个进程(先不谈多线程),从操作系统的层面看,我们使用打印机的步骤有如下:1)使用CPU执行程序,去硬盘读取需要打印的文件,然后CPU会长时间的等待,直到硬盘读写完成;
2)使用CPU执行程序,让打印机打印这些内容,然后CPU会长时间的等待,等待打印结束。
在这样的情况下:其实CPU的使用率其实非常的低。打印一个文件从头到尾需要的时间可能是1分钟,而cpu使用的时间总和可能加起来只有几秒钟。而后面如果单进程执行游戏的程序的时候,CPU也同样会有大量的空闲时间。**使用多进程后:**当CPU在等待硬盘读写文件,或者在等待打印机打印的时候,CPU可以去执行游戏的程序,这样CPU就能尽可能高的提高使用率。再具体一点说,其实也提高了效率。因为在等待打印机的时候,这时候显卡也是闲置的,如果用多进程并行的话,游戏进程完全可以并行使用显卡,并且与打印机之间也不会互相影响。3、总结进程直观点说是保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。二、什么是线程?****1、基本常识早期操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。线程是程序执行中一个单一的顺序控制流程:1)程序执行流的最小单元
2)处理器调度和分派的基本单位
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
如上图所示,在任务管理器的进程一栏里,有道词典和有道云笔记就是进程,而在进程下又有着多个执行不同任务的线程。2、任务调度线程是什么?要理解这个概念,需要先了解一下操作系统的一些相关概念。大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式。在一个进程中:当一个线程任务执行几毫秒后,会由操作系统的内核(负责管理各个任务)进行调度,通过硬件的计数器中断处理器,让该线程强制暂停并将该线程的寄存器放入内存中,通过查看线程列表决定接下来执行哪一个线程,并从内存中恢复该线程的寄存器,最后恢复该线程的执行,从而去执行下一个任务。上述过程中任务执行的那一小段时间叫做时间片,任务正在执行时的状态叫运行状态,被暂停的线程任务状态叫做就绪状态,意为等待下一个属于它的时间片的到来。这种方式保证了每个线程轮流执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发(别觉得并发有多高深,它的实现很复杂,但它的概念很简单,就是一句话:多个任务同时执行)。
3、进程与线程的区别****进程与线程的关系1)线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
2)一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
3)进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
4)线程上下文切换比进程上下文切换要快得多。
▲ 进程与线程的资源共享关系
▲ 单线程与多线程的关系总之线程和进程都是一种抽象的概念,线程是一种比进程更小的抽象,线程和进程都可用于实现并发。在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。它相当于一个进程里只有一个线程,进程本身就是线程。所以线程有时被称为轻量级进程。后来随着计算机的发展,对多个任务之间上下文切换的效率要求越来越高,就抽象出一个更小的概念——线程,一般一个进程会有多个(也可以是一个)线程。
4、多线程与多核上面提到的时间片轮转的调度方式说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。很多操作系统的书都说“同一时间点只有一个任务在执行”。其实“同一时间点只有一个任务在执行”这句话是不准确的,至少它是不全面的。那多核处理器的情况下,线程是怎样执行呢?这就需要了解内核线程。多核(心)处理器是指在一个处理器上集成多个运算核心从而提高计算能力,也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。内核线程(Kernel Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以在操作系统中看到的CPU数量是实际物理CPU数量的两倍,如你的电脑是双核四线程,打开“任务管理器 -> 性能”可以看到4个CPU的监视器,四核八线程可以看到8个CPU的监视器。超线程技术就是利用特殊的硬件指令,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率。这种超线程技术(如双核四线程)由处理器硬件的决定,同时也需要操作系统的支持才能在计算机中表现出来。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Lightweight Process,LWP),轻量级进程就是通常意义上所讲的线程,也被叫做用户线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。用户线程与内核线程的对应关系有三种模型:1)一对一模型;
2)多对一模型;
3)多对多模型。
5、一对一模型对于一对一模型来说:一个用户线程就唯一地对应一个内核线程(反过来不一定成立,一个内核线程不一定有对应的用户线程)。这样如果CPU没有采用超线程技术(如四核四线程的计算机),一个用户线程就唯一地映射到一个物理CPU的内核线程,线程之间的并发是真正的并发。一对一模型优点
使用户线程具有与内核线程一样的优点一个线程因某种原因阻塞时其他线程的执行不受影响(此处,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现)。一对一模型缺点1)许多操作系统限制了内核线程的数量,因此一对一模型会使用户线程的数量受到限制;
2)许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。
