浅谈同步异步、阻塞非阻塞的含义和区别以及组合在代码中的体现:
- 这篇文章的由来:众多博客中一句“线程池+任务队列实现伪异步”搞得我晚上躺在床上理结构理到两点(两点疑惑:为什么同步阻塞是伪异步、单纯的多线程是吗)。
- 网上很教科书的定义/回答(太抽象了,很难和代码对应)已经很多了,所以本博客以“民科”的方式来讲讲自己的理解,如果有误还请指正。
一、首先我们回想网络通信的结构(避免混乱,只看服务端等待读就绪的情况):
- 要理解这4个词,这个结构是非常重要的!
- 客户端:发起请求
- 服务端主线程:接收请求,发起调用处理请求
- handle_connect():被调用的函数【主要构成:两个阻塞函数read()、write()】(可以在主线程、也可以在子线程执行)
二、什么是同步、异步?如何区分?
- 同步异步说的是什么:
同步、异步说的是handle_connect()这个函数(一个调用)给主线程(调用者)的返回状态。 - 如何区分同步异步:
如何区分同步、异步要从 调用是否立即return+调用处理好请求后产生的结果是否通知调用方 两个方面判别。- 同步:
- 被调用的函数 何时return:一个调用(eg. handle_connect())被发起后,只有执行完函数才return。
所以这个return是处理完的结果! - 调用者 如何查看 被调用的函数 对客户端请求的处理结果:被调用的函数做完事就完工了,结果就放在那,调用方需要自己来看。
即如果此时你(主线程)不阻塞等我(handle调用)执行完,你是不知道我什么时候完成任务的。你就必须过一会来看我一下有没有做好。
- 被调用的函数 何时return:一个调用(eg. handle_connect())被发起后,只有执行完函数才return。
- 异步:
- 被调用的函数 何时return:一个调用(eg. handle_connect())被发起后,调用立刻return!(
这个return的不是处理完的结果!而是一种状态应答!表示我知道你布置给我任务了!我做完了告诉你!)- 所以调用是异步的情况下,主线程也就不存在等待一说(阻塞),主线程调用完handle后,handle保证立刻返回,主线程也就必然继续往下执行了----->即
异步阻塞毫无意义 - 所以调用是异步下,必然要存在另一个实际存在的线程在执行这个handle_connect()调用,并且执行完之后会告诉主线程
真正的请求结果。
- 所以调用是异步的情况下,主线程也就不存在等待一说(阻塞),主线程调用完handle后,handle保证立刻返回,主线程也就必然继续往下执行了----->即
- 调用者 如何查看 被调用的函数 的 return结果:被调用的函数(handle)做完事就完工了,会通过事件+回调来主动告诉调用方(主线程)来看自己的结果。
- 被调用的函数 何时return:一个调用(eg. handle_connect())被发起后,调用立刻return!(
- 同步:
一句话总结:同步异步就是在说,主线程调用handle_connect()后,handle_connect()会不会立刻return!。
三、什么是阻塞、非阻塞?如何区分?
- 阻塞非阻塞说的是什么:
阻塞、非阻塞说的是主线程是否等待handle_connect()的return(即主角是调用方,即主线程) - 如何区分阻塞非阻塞:
看主线程是否等待调用return后才去做别的事。- 阻塞:主线程调用handle()后,等他return了才继续做别的事(不管调用是同步还是异步,只关心函数层面的return)。所以
阻塞的主线程更像是对调用的通知+监工,你去把这个事情做一下,然后我看着你做。 - 非阻塞:主线程调用handle()后,不管他有没有return,都直接紧接着做别的事。所以
非阻塞的主线程更像是对调用的一种通知,你去把这个事情做一下,然后自己就干别的去了。
- 阻塞:主线程调用handle()后,等他return了才继续做别的事(不管调用是同步还是异步,只关心函数层面的return)。所以
四、用一个渐进的情景例子来理解同步异步+阻塞非阻塞的各种组合,并分析代码:
- 情景:有以下3个角色和一个具体的执行任务
- 客户端----------------->客户
- 服务端主线程----------->包工头
- handle_connect()------>具体的项目操作
- 服务端子线程----------->包工头手下的工人
- NOTE:这里
多线程时会有点绕,此时要理解为主线程执行的调用是pthread_create():创建一个子线程并让他运行指定函数(招聘一个工人,并告诉它做什么)。
同步阻塞(BIO):
- 单线程:公司成立初期,公司里只有包工头一个人。这时候有客户来谈项目(请求连接),包工头接到项目(连接建立)之后,自己动手在主线程内调用handle_connection()把这个项目做完。
- 同步:handle_connection()只有做完事才会return
- 阻塞:handle_connection()执行的时候,主线程只能等待。
- 多线程:包工头发现这样不行啊,做一个客户项目的时候,其他客户来了没人招待只能干等着,于是他决定外包项目,自己只负责接项目,接到项目后外包给工人去做。
- 对包工头而言:调用是pthread_creat(),找个临时工过来,告诉他干什么活,然后让他自己去干----->
代表让一个工人(子线程)去干活(执行handle),并且我不等你干完,我只是给你分配完这个任务,我就继续去接待其他客户了。- 伪异步:因为包工头不再亲手执行handle_connection()了,而是每次接到项目,调用pthread_create()把项目外包给临时工去做。所以此时对主线程而言,调用是pthread_create():创建创建子线程并告诉它干什么,这个调用是会立刻返回的。但对于要处理这个任务而言,任务是未完成的(工人独立的在那做)。即主线程(包公头)执行完调用pthread_create()后,得到的返回值是工人回复他的一个状态(收到),然后工人再去执行具体的任务。
- 非阻塞:主线程对于这个任务的处理而言肯定是非阻塞的,因为任务还没完成(子线程在那干着呢),包工头已经去接待下一位客户了。但主线程是会阻塞在accept()处的。
- 对工人而言:调用是handle_connection()
- 同步:handle_connection()只有做完事才会return
- 阻塞:handle_connection()执行的时候,子线程只能等待。、
- 对包工头而言:调用是pthread_creat(),找个临时工过来,告诉他干什么活,然后让他自己去干----->
- 总结:为什么叫伪异步:
- 主线程的调用会立刻返回一个状态,告诉主线程,好的我知道这个任务了,然后让另一个独立的线程去做。------->所以从主线程的角度来看:
调用对于主线程而言是异步的(因为调用,先返回ack状态,再去独立执行任务) - 那为什么是伪呢?因为具体在执行每个任务的子线程都是同步阻塞的,即该模板的底层还是同步阻塞模型。并且
主线程虽然不在处理任务处阻塞,但还是会在等待任务时阻塞(与IO多路复用的区别,线程不会阻塞,无论是等待还是读,因为自己不等待,交给select系统调用去做了,并且读就绪的时候,一定是完整的数据,可以直接读完就结束会话)。
- 主线程的调用会立刻返回一个状态,告诉主线程,好的我知道这个任务了,然后让另一个独立的线程去做。------->所以从主线程的角度来看:
- 线程池:包工头觉得每次有活了再租临时工的话频繁的签约解约太贵了(频繁的创建、销毁线程),所以它决定直接和m个工人签一份按工作时间计算工资的长约(即程序开头创建m个线程)。然后建立一个系统(任务队列),包工头把签到的任务放进去,所有的工人自己无限循环的去查看这个系统,抢任务做。
- 对包工头而言:调用是enqueue(),把任务放进系统
- 伪异步:同多线程
- 非阻塞:同多线程
- 对工人而言:调用是handle_connect(),执行具体的任务
- 同步:同多线程
- 阻塞:同多线程
- 对包工头而言:调用是enqueue(),把任务放进系统
Question:所以个人理解(从线程池和多线程的模型对比中可以看出),只要是Reactor模式都是伪异步,而不是只有线程池+任务队列才是伪异步。
- 信号量:包工头觉得如果让工人自己无限循环去抢活干太花钱了,毕竟是按工作时间付费的。所以他决定给工人定个规矩,如果你去系统里找任务时,系统是空的(任务队列中无任务),那你就在系统门口排队睡觉吧(不给你钱了,即工人不占用CPU)。如果包工头自己接到任务了,那就根据等待时间和优先级去唤醒一个在系统门口睡觉的线程。这样工人的工资又少了很多(子线程CPU占用低),包工头开心了。
- 3和4的模式和多线程的IO模型是一样的,都是Reactor模式,所以就都看2中的分析即可,它们只不过是对服务端对CPU占用的一个优化改进。
同步非阻塞(NIO):
- NIO的底层是通过IO多路复用模型实现的(select,poll,epoll),使得一个线程能够处理多个任务。
- 等待任务:此模型下,线程不再自己等待任务,而是将监视任务交给select()去做,select()会监视listenfd所指的文件,一旦里面有文件读就绪,就会通知线程。
- 执行任务:线程被select()通知有读就绪/连接就绪后,就要轮询监听集合,看看是哪个文件好了,然后去执行相应的任务(如果是连接就绪,就把connectfd放入select的监听集合,如果是读就绪,就执行handle,然后移出集合)。
- 所以对于一个线程而言:
- 同步:调用是handle_connect(),而该函数是同步的(有结果才返回)
- 非阻塞:线程执行handle_connect()时不会阻塞在这里,因为select()保证了执行handle_connect()时,文件中的数据是完整的了,线程直接读完就能干别的事了,所以看起来是非阻塞的。(底层其实还是同步阻塞的实现,只不过保证了服务端读时不用等待,因为数据是发完的完整数据,所以看起来非阻塞)。
学些中记录,如果有误还请指出。