并发-0-同步/异步/阻塞/非阻塞/进程/线程

同步、异步、阻塞、非阻塞,这四个概念很少人可以说清楚,不信的话,你可以先自己试着写下来

同步和异步

关注维度:消息的通信机制(synchronous communication/asynchronous communication)

判断标准:调用者是否主动等待被调用者的返回结果

同步的理论说明:任务A的执行过程中调用了任务B。任务A对任务B发起调用后,主动等待调用结果。

同步的生活举例:你去书店问老板,是否有《操作系统》这本书,老板说:稍等,我查一下。然后开始查啊查,等查好了,告诉你结果(主动等待被调用方返回结果)。

异步的理论说明:任务A的执行过程中调用了任务B。任务A对任务B发起调用后,继续执行后续工作。任务B完成后通过状态、通知来通知调用者。

异步的生活举例:你去书店问老板,是否有《操作系统》这本书,查好了打电话给你,然后直接挂电话了(此时被调用方不返回结果)。过了几天,查好了,老板主动打电话给你(被调用方回调调用方,告知结果)。
复制代码

阻塞和非阻塞

关注维度:任务在等待调用结果时的状态

判断标准:调用方在等待被调用方的返回结果时,是否可以做其他事(是否被挂起)

阻塞的理论说明:任务A对任务B发起调用后,任务B需要执行一段时间才可返回结果,任务A选择等待任务B的返回结果(暂时挂起)。

阻塞的生活举例:你去书店问老板,是否有《操作系统》这本书,你会一直把自己挂起,什么是后不干,一直在那等,直到得到返回结果。

非阻塞的理论说明:任务A对任务B发起调用后,与此同时,任务A在任务B执行的过程中去完成别的工作,等待任务B结果返回后再继续(不挂起,而是继续执行自己的任务)。

非阻塞的生活举例:你去书店问老板,是否有《操作系统》这本书,不管老板有没有告诉你,你自己都先去玩了(继续执行自己的任务而不是干等),但是也要偶尔也要check一下老板是否有了结果。
复制代码

我们把用户线程当做调用者,把内核线程当做被调用者,用几张图和简单的示例代码描述一下当前流程的几种I/O模型:

同步阻塞IO:

read(socket, buffer)
process(buffer)
复制代码

同步非阻塞IO:

while(read(socket,buffer) != SUCCESS);
process(buffer);
复制代码

IO多路复用:

select(socket);
while(1){
    sockets = select();
    for(socket in sockets){
        if(canRead(socket)){
            read(socket,buffer);
            process(buffer);
        }
    }
}
复制代码

虽然这种方式允许在单个线程中处理多个IO请求,但是每个IO请求的过程还是阻塞的,平均时间甚至比同步阻塞IO模型要更长

Reactor模式:

和多路IO复用的差别在于,通过Reactor的方式,可以将用户线程轮询IO操作状态的工作交给Reactor的事件循环进行处理,用户线程注册事件处理器之后可以继续执行其他的工作,Reactor线程负责调用内核的select函数检查socket状态

UserEventHandler handleEvent(){
    if(canRead(socket)){
        read(socket,buffer);
        process(buffer);
    }
}

Reactor.register(new UserEventHandler(socket));

Reactor.epollHandleEvents(){
    while(1){
        sockets = select();
        for(socket in sockets){
            getEventHandler(socket).handleEvent();
        }
    }
}
复制代码

IO多路复用还是使用了会阻塞线程的select系统调用,最多只能算异步阻塞IO,而非真正的异步IO

异步非阻塞IO:

在异步阻塞IO中,用户线程收到通知后自行读取数据、处理数据。而在异步非阻塞IO中,用户线程收到通知时,数据已经被准备好,用户线程可以直接使用(省略了读取数据这一过程)

UserCompeletionHandler.handleEvent(buffer){ process(buffer); }

aioRead(socket, new UserCompeletionHandler());

进程是什么:

计算机最初发明的初衷是用于解决耗时耗力的复杂计算,是一个计算器

最原始的计算机执行程序的过程如下:等待用户输入指令->用户输入->计算机操作->等待用户输入指令->用户输入->计算机操作。在用户思考或者输入的过程中,计算机就空闲下来。

后来有了批处理系统,用户可以把许多指令(如输入1,输入2)写在磁盘中,计算机的执行过程变为:用户输入指令集合->取指令->执行->取指令->执行。        

批处理系统大大提高了便捷性,但是还是存在一个问题,假设指令集合中有A,B两个程序,当程序A进行I/O处理时,程序B只能等待程序A直到其运行完。也就是说,内存中只能有一个程序在执行。
复制代码

           |程序One|程序Two|    计算机中有两个程序

Time1:     |内存|              内存中装载程序One

Time2:             |内存|      内存中装载程序Two

复制代码
那么,如何在内存中装入多个程序呢?于是人们发明了进程,每个在运行的程序都看做一个进程,给每个进程分配合适大小的对应的内存地址空间,进程之间的空间互不干扰,并且保存每个进程的运行状态。通过进程之间的相互切换,使计算机看起来在一段时间内有几个程序在同时执行。   
复制代码
           |程序One|程序Two|    计算机中有两个程序

Time1:    |部分内存|部分内存|    内存中装载程序One,Two,分别在不同的部分。

复制代码
进程让程序之间的并发成为了可能,从宏观上看,某个时间段内有多个程序在同时执行,但实际上在某一时刻只有一个程序(一部分的内存)会得到CPU,进行执行。
复制代码

线程是什么:

进程让程序之间的并发成为了可能,我们可以在电脑上同时听歌,打字了(两个不同的程序之间切换)。
复制代码

但是人们对程序实时性的要求越来越高。比如对QQ音乐来说,它不仅要处理用户所发送的交互请求,还要播放歌曲。假设某一时刻QQ音乐在播放歌曲,你点击了“暂停”按钮,需要等待播放歌曲完毕之后才能处理“暂停”操作,这种程序肯定是不合格的。

于是人们把QQ音乐这个程序所对应的进程拆分成了多个线程,有播放歌曲的线程,处理交互请求的线程,每个线程负责一个独立的子任务,这样的话,我们点击了“暂停”按钮,QQ音乐会释放暂停播放歌曲的线程,让交互请求的线程处理用户的请求,响应完之后再切换回来,让播放歌曲的线程得到CPU。具体过程如下:

 播放线程---------------------挂起`````````````````播放线程----------     

                  用户点击暂停

                                  交互线程--------处理完毕
复制代码
线程让进程内的子任务并发成为了可能。
复制代码

3.程序,进程,线程:

程序是我们写的代码,需要对应到一个具体的进程来运行,进程间有独立的内存地址,互不干扰。线程是进程的子任务,属于同一进程的线程共享相同的内存。

进程让程序之间的并发成为了可能,线程让进程内的子任务并发成为了可能。

猜你喜欢

转载自juejin.im/post/5bc69ecee51d45395d4f4072