TOC
同步/异步
在实际的开发中,经常会听到同步、异步、阻塞、非阻塞这些编程概念,这四个技术在计算机领域占有非常重要的地位,同步和异步其实指的是,请求发起方对消息结果的获取是主动发起的,还是等被动通知的,他们描述的是消息通信的机制;
对于同步来讲,调用方发起调用,那么在没有得到结果之前,该调用就不返回,代码就定格在等待结果这里,无法继续往下执行,但调用一旦返回,就能进行下一步的处理;
而对于异步来讲,它和同步相反,调用在发出之后,这个调用就直接返回了,但是返回的不是"最终的"结果,可能是中间结果或者是一个状态。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数来告知调用方结果,此时调用方并不会等待结果的返回,而是继续往下执行,当收到调用结束的通知时再继续处理;
异步消息通知机制
对于通知调用者的三种方式,具体如下;
状态:即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低;
通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能;
回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数;
中间结果
其实同步异步和我们的进程池很像,我们在使用multiprocessing.Pool去创建进程池时,如果调用apply_sync会处于同步状态,主进程会等待子进程结束之后才能进行下一步工作,这就是一个同步的事物,但是如果我们使用apply_async,我们主进程就不需要去等待这个子进程执行完成,一样可以进行下一步的工作,并且通过callback的方式来通知调用方,这就是一个异步的事物;
那么什么是中间结果呢,可以看到如下代码,我们使用apply_async来开启一个异步任务,异步任务会拿到一个result返回值,通过结果可以看到,这个result返回值是一个ApplyResult对象,返回的是一个result对象,它并非最终结果,这就是中间值;
def calc():
sum=0
for i in range(90000000):
sum+=1
pool=multiprocessing.Pool(processes=4,)
for _ in range(4):
result=pool.apply_async(calc,)
print(result) # ApplyResult对象
pool.close()
pool.join()
# <multiprocessing.pool.ApplyResult object at 0x10545d160>
# <multiprocessing.pool.ApplyResult object at 0x10545d240>
# <multiprocessing.pool.ApplyResult object at 0x10545d320>
# <multiprocessing.pool.ApplyResult object at 0x10545d3c8>
- 注意:
所以,同步异步在编程个范围内的解释就是,请求发起方发起请求时能不能直接拿到请求的"最终"数据,如果能拿到请求的"最终"数据,那么这就是一个同步请求, 如果拿不到想要的数据,或者不是最终的数据(有可能是一个中间结果),那么这个请求就是一个异步请求;
- 重点:
关注的是消息通知机制,同步异步的主要区别就是调用者是否能够立即得到最终结果;
阻塞/非阻塞
同步异步强调的结果,最终的结果,阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态.,同步异步关注结果最终结果,如果是阻塞,那就说明调用者只能死等下去,在等待的过程中什么其他任务都不能执行,而非阻塞,则调用者在等待时可以先去处理其他的任务,不用一直干等下去;
- 重点:
关注的是等待消息时自己的状态,阻塞与非阻塞主要区别就是强调调用者是否需要一直等待;
组合
同步、异步、阻塞、非阻塞,两两结合就能形成四种常见的IO模型,最常使用的就是同步阻塞和异步非阻塞;
同步阻塞:调用者向被调用者发起请求,在没拿到请求结果之前,将一直等待,无法继续往下执行,直至获得所需的资源。
同步非阻塞:调用者向被调用者发起请求,被调用者会返回一个状态值,如,处理完成为True,未处理完成为False,调用者不会在此等待,会继续往下执行,但是调用者会周期性反复查看这个状态值是否为真;
异步阻塞:调用者向被调用者发起请求,被调用者返回了一个状态值,但是调用者却还是一直等待结果返回;
异步非阻塞:类似回调函数,当调用者发起调用之后,就继续处理其他事情,调用者处理完成之后,通过回调函数来通知调用者;
# 举例
同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成;
同步体现在:等待下载完成通知;
阻塞体现在:等待下载完成过程中,不能做其他任务处理;
同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成;
同步体现在:等待下载完成通知;
非阻塞体现在:等待下载完成通知过程中,去干别的任务了,只是需要时不时会瞄一眼进度条,小明必须要在两个任务间切换,关注下载进度;
异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就"叮"一声。不过小明不做别的事,仍然一直等待“叮”的声音;
异步体现在:下载完成"叮"一声通知;
阻塞体现在:等待下载完成“叮”一声通知过程中,不能做其他任务处理;
异步非阻塞:仍然是那个会"叮"一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了;
异步体现在:下载完成"叮"一声通知;
非阻塞体现在:等待下载完成"叮"一声通知过程中,去干别的任务了,只需要接收"叮"声通知即可,软件处理下载任务,小明处理其他任务,不需关注进度,只需接收软件“叮”声通知,即可;
也就是说,同步/异步是"下载完成消息"通知的方式(机制),而阻塞/非阻塞则是在等待"下载完成消息"通知过程中的状态(能不能干其他任务),在不同的场景下,同步/异步、阻塞/非阻塞的四种组合都有应用;
所以,综上所述,同步和异步仅仅是关注的消息如何通知的机制,而阻塞与非阻塞关注的是等待消息通知时的状态。也就是说,同步的情况下,是由处理消息者自己去等待消息是否被触发,而异步的情况下是由触发机制来通知处理消息者,所以在异步机制中,处理消息者和触发机制之间就需要一个连接的桥梁。在小明的例子中,这个桥梁就是软件"叮"的声音;
操作系统知识
Intel的x86处理器是通过Ring级别来进行访问控制的,级别共分4层,RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3。RING0层拥有最高的权限,RING3层拥有最低的权限。按照Intel原有的构想,应用程序工作在RING3层,只能访问RING3层的数据,操作系统工作在RING0层,可以访问所有层的数据,而其他驱动程序位于RING1、RING2层,每一层只能访问本层以及权限更低层的数据。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息;
RING设计的初衷是将系统权限与程序分离出来,使之能够让OS更好的管理当前系统资源,也使得系统更加稳定。举个RING权限的最简单的例子:一个停止响应的应用程序,它运行在比RING0更低的指令环上,你不必大费周章的想着如何使系统恢复运作,这期间,只需要启动任务管理器便能轻松终止它,因为它运行在比程式更低的RING0指令环中,拥有更高的权限,可以直接影响到RING0以上运行的程序;
当然有利就有弊,RING保证了系统稳定运行的同时,也产生了一些十分麻烦的问题。比如一些OS虚拟化技术,在处理RING指令环时便遇到了麻烦,系统是运行在RING0指令环上的,但是虚拟的OS毕竟也是一个系统,也需要与系统相匹配的权限。而RING0不允许出现多个OS同时运行在上面,最早的解决办法便是使用虚拟机,把OS当成一个程序来运行。后来才有了更新的技术解决了此问题;
Ring0级:这里面就是一些特权级别指令,可以访问所有级别数据,也可以访问I/O设备等;
Ring3级:最低级别,只能访问本级数据;
物理内存
内核代码运行在Ring0,用户代码运行在Ring3,访问一些硬件设备,比如I/O设备,往往得到驱动层的支持,也就是Ring0级,换句话说,我们要读取一个文件,或者读取一个网络I/O,这个时候就不行进程能干的事了,这个时候,就得向操作系统的内核发起请求,让操作系统来帮助进行将这个事情完成,而操作系统很多的指令都是在Ring0级别的;
现代操作系统采用了虚拟存储器,在早起的计算机可能计算机才2G的内存,但是你会发现2G的一个内存一样可以很流程的使用,甚至有的时候还能使用超过2G的内存,这个时候一样可以使得操作系统正常的运行;
究其原因是因为,对于操作系统来讲,内存其实分为两种,一种叫做物理内存,就是我们所理解的实打实的的内存卡,还有一种叫虚拟内存空间,虚拟内存空间实际上是一个进程认为自己能够访问的空间,每一个进程都认为,自己能够独占当前操作系统上的所有内存资源,其实它看到的是一个假象,这个时候内存就是一种虚拟的存储;
那么这个内存的高地址,有一部分是被操作系统占用的,所以我们早起在使用Windows32位系统时,我们会发现虽然我们机器上有4G内存,但是实际上有1G的内存我们无法使用,应用程序只能使用剩下的那3G,也就是说,在32位系统时,每个进程自以为自己能够使用4G内存,实际上根本无法使用这么多;
虚拟内存
那么当物理内存耗尽时,这个时候我们就需要利用到另一种内存,即虚拟内存,这个虚拟内存往往是一个磁盘上的文件,在unix下叫做swap,它实际上就是文件系统上的一个分区,当物理内存真的不够使用时,会将虚拟内存和物理内存拼接起来,一起向操作系统提供内存需求,但是这带来的问题是严重的,因为内存是高I/O设备,但是swap是一个磁盘,一个低I/O设备,所以会带来严重的性能问题,这是在不得已下才会使用;
应用程序
操作系统的内核代码,都是运行在Ring0这个特权区域的,这里面的指令是不允许低级别程序直接使用,它们拥有访问所有硬件设备的所有权限,所以Ring0这部分我们称之为内核态,也称之为内核空间;
普通的应用程序都是运行在用户空间的(用户态),即Ring3,那么在用户空间的应用程序,如果想访问磁盘上的某一个文件时,它不能自己直接去访问磁盘上的文件,它没有权限,它必须调用操作系统提供的系统调用;
此时有两种方式,方式一,直接去调用系统提供的编程接口,这个编程接口叫系统调用,即System Call Interface,但是这个太难调用了,索一大多数情况下我们都不会使用方式一 ,那么方式二就来了,我们在系统调用之上方一个C语言写的库,这个库是在用户空间的,用户可以直接调用这个库,然后由这个C写的库去转而调用System Call Interface,这样我们的操作就简化了;
在前面的Socket的章节,我们说过sendfile的概念,当一个进程在请求读取一个文件时,首先运行在用户空间的进程,会向操作系统发起系统调用,然后由内核将数据读取到内核的内存空间,然后内核会将内核的内存空间的数据拷贝到用户空间去,所以说,内核空间的数据是一个临时数据,数据最终都会拷贝到用户空间去,这样应用程序才能使用它;
那么如果我们现在需要对端主机发送一个数据,发送数据会用到网络子系统,那么在这种场景下,首先我们用户空间的进程必须请求操作系统(内核)来读取这个数据,然后内核会将这个数据读取到自己的内存空间,然后复制到用户空间,那么又因为,我们需要发送数据,所以我们又必须将这个数据发送到内核空间的内存空间去,然后调用操作系统网络子系统将这个数据从网络接口发送出去,所以这里面就存在的数据的多余复制,那么sendfile就省掉了一次拷贝的过程,sendfile是当内核将这个数据读取出来之后,直接从网络接口发送到对端主机,不用再拷贝到用户空间一趟;
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化;
1、保存处理机上下文,包括程序计数器和其他寄存器;
2、更新PCB信息;
3、把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列;
4、选择另一个进程执行,并更新其PCB;
5、更新内存管理的数据结构;
6、恢复处理机上下文;
文件描述符
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念,文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统;