北川广海の梦

北川广海の梦

浅谈异步与多线程

650
2020-03-04

从刚开始编写程序开始,就知道我们写的程序,最终会变成计算机能识别的指令,一个一个的被得到执行。但是假如我们有多个任务,就需要按照顺序,一个接一个的进行执行,假设我们当前任务前面,有一个非常耗时的任务,那么将会大大增加我们的等待时间。 这样的执行方式,就是同步执行

同步执行与批处理操作系统

在上个世纪50年代的批处理操作系统中,采用的就是这样的执行任务方式。计算机能够成批的一个接一个的进行自己的任务处理。但是这样的问题前面已经说的很清楚,排在后面的任务并不能被快速的进行执行,即使排在后面的任务不需要进行复杂的计算工作。而且对于一些特殊的不需要消耗CPU的工作,例如I/O,也会导致CPU在盲目的进行等待,这样就大大的降低了程序的工作效率。

异步与多道程序

为了解决批处理操作系统的缺陷,于是出现了多道程序系统,这种操作系统最大的特点与进步,就在于大大改进了在不需要CPU的任务中的操作效率。例如现在A程序,需要从磁盘中进行数据读取操作。这是一个非常耗时的操作,然而却并不需要CPU的运算能力进行参与。那么此时A完全可以将CPU的控制权解放出去,交给其他的任务,例如B(需要CPU进行运算),那么B只需要在A进行I/O等待的时候,就能完成自己的任务。当B完成之后,再回到A的还原点,就能继续执行A任务。这样的在A进行,但却没有结束,B也得到执行的方式,就是异步执行。最早在多道程序操作系统中得到体现。

多线程与分时操作系统

多道程序操作系统大大改进了I/O操作任务的执行效率。但是现在假设A任务,并不是一个能解放CPU的任务,而是一个复杂的计算密集型任务,CPU必须大量投入其中,无暇估计其他,那么在多道程序系统中,B任务即使非常简单,只需要计算1+1等于多少,也不得不排在先来的A任务之后。为了解决这一问题。人们设计出了分时操作系统。 分时操作系统最大的特点就是CPU时间片轮换,将CPU的执行时间分成很多很多的小碎片,给每一个任务都进行分配相等的时间片,如果任务执行完成,那么将不会再得到时间片,如果在时间片之内没有完成任务,也只能先对当前任务状态进行保存,等到下一次分配到时间片的时候,再继续执行。 CPU的时间片可以等价于CPU使用权。 这样执行方式,无疑会大大的降低CPU的运行效率,因为时间片轮换是非常损耗性能的,那么为什么还要这样设计呢? 前面提到,假设A任务非常耗时,而B任务却很简单,我们自然不希望B任务等待那么久的时间。所以在A任务执行一会的时候,CPU抽出身来,执行一下B任务,就能让等待B任务的人很快就得到结果,同时也给人一种错觉,好像CPU就专为我一个人服务的样子。但我们都知道,CPU是个渣男无疑。

线程

为了更优雅的利用CPU,人们发明了线程这一概念,每一个线程,都可以看作一个任务(线程与进程的区别这里不再提及),通过多线程,CPU就能为多个线程都分配时间片,让每一个线程任务都能得到执行。有了这一强大的工具,我们就能在写程序的时候,轻松的进行异步执行。然而,多线程离不开操作系统的支持,无论我们是C++还是Java,亦或者是.NET,其中都有线程的概念,但他们的底层,都一定是调用了操作系统提供的多线程相关的API。

多线程与异步

通过上文的介绍,想必大家已经知道,多线程与异步的关系了。多线程其实是异步的一种具体的实现方式。而异步,其实是一种程序执行的方式。异步并不一定是需要多线程的。例如JavaScript,由于他肩负着重要的任务,例如与用户互动,操作DOM,注定了它只能是单线程的(否则会带来非常麻烦的资源共享问题)。然而,我们打开一个网站,里面可能有成千上万个JavaScrpit任务,然而网站却非常流畅,用户根本没有卡顿的感觉。除了Google的V8引擎在性能上的加持外,浏览器中绝大多数耗时任务,都是来自于网络I/O,自然不需要等待这些任务。笔者曾经试图在浏览器环境下,加载一个CAD文件,并转换成可视化的图像,很显然这是一个非常需要性能的任务。不意外的,在加载过程中,整个窗口都失去了响应,这就是因为JavaScrpit只有一个单线程。

计算密集型任务与I/O密集型任务

上文中举到的例子,可以总结为两类,一种是计算密集型,意味着需要大量CPU进行参与,消耗CPU算力。另一种就是I/O密集型任务,意味着不需要CPU大量参与,在进行I/O操作时,CPU处于空闲状态。 对于不同的任务,我们编程的处理方式也就不同。 计算密集型任务,由于CPU任务负担非常中,我们不希望编程语言的执行速率进行影响和拖累,因为这是非常不划算的。所以对于这类任务,尽可能的选择高效率的执行语言,例如C/C++,甚至直接用汇编进行编写。 而I/O密集型任务,程序语言的执行效率反而不是重点,因为你的程序可能用了0.1秒才执行到了需要I/O等待的地方,I/O操作却可能需要五六秒的时间,想必之下,程序的执行效率就显得不重要了,而重要的是如何优雅的将我们的程序编写为异步程序,提高开发的效率。 例如在Web服务器开发中,绝大多数任务都是对数据库的操作,这是非常耗时且典型的I/O操作。所以我们一般都会选择编写容易,对异步执行友好的语言,例如.NET Java Python等等,而单线程的JavaScrpit也完全能胜任这个任务,甚至能比其他一些语言做的更好。而假如我们确实有对复杂计算的需求,为了保证程序的开发效率,我们也可以将这样的复杂计算任务,做成RPC的方式,让其他主机,例如拥有32核64线程的超级服务器(笑),去完成计算任务。而在我们本地的开发中,只需要将这个任务当作是一个从网络取会运算结果的I/O操作就可以了。这样的系统架构,既能保证我们开发效率,也能轻松的完成复杂的计算任务,不让计算任务影响我们服务器的吞吐量。