浅谈同步异步、阻塞非阻塞的含义和区别以及在代码中的体现:

393 阅读9分钟

浅谈同步异步、阻塞非阻塞的含义和区别以及组合在代码中的体现:

  • 这篇文章的由来:众多博客中一句“线程池+任务队列实现伪异步”搞得我晚上躺在床上理结构理到两点(两点疑惑:为什么同步阻塞是伪异步、单纯的多线程是吗)。
  • 网上很教科书的定义/回答(太抽象了,很难和代码对应)已经很多了,所以本博客以“民科”的方式来讲讲自己的理解,如果有误还请指正

一、首先我们回想网络通信的结构(避免混乱,只看服务端等待读就绪的情况):

  • 要理解这4个词,这个结构是非常重要的!
  1. 客户端:发起请求
  2. 服务端主线程:接收请求,发起调用处理请求
  3. handle_connect():被调用的函数【主要构成:两个阻塞函数read()、write()】(可以在主线程、也可以在子线程执行)

二、什么是同步、异步?如何区分?

  1. 同步异步说的是什么:同步、异步说的是handle_connect()这个函数(一个调用)给主线程(调用者)的返回状态
  2. 如何区分同步异步: 如何区分同步、异步要从 调用是否立即return+调用处理好请求后产生的结果是否通知调用方 两个方面判别
    • 同步:
      1. 被调用的函数 何时return:一个调用(eg. handle_connect())被发起后,只有执行完函数才return。所以这个return是处理完的结果!
      2. 调用者 如何查看 被调用的函数 对客户端请求的处理结果:被调用的函数做完事就完工了,结果就放在那,调用方需要自己来看。即如果此时你(主线程)不阻塞等我(handle调用)执行完,你是不知道我什么时候完成任务的。你就必须过一会来看我一下有没有做好。
    • 异步:
      1. 被调用的函数 何时return:一个调用(eg. handle_connect())被发起后,调用立刻return!这个return的不是处理完的结果!而是一种状态应答!表示我知道你布置给我任务了!我做完了告诉你!
        • 所以调用是异步的情况下,主线程也就不存在等待一说(阻塞),主线程调用完handle后,handle保证立刻返回,主线程也就必然继续往下执行了----->即异步阻塞毫无意义
        • 所以调用是异步下,必然要存在另一个实际存在的线程在执行这个handle_connect()调用,并且执行完之后会告诉主线程真正的请求结果
      2. 调用者 如何查看 被调用的函数 的 return结果:被调用的函数(handle)做完事就完工了,会通过事件+回调来主动告诉调用方(主线程)来看自己的结果。
  • 一句话总结:同步异步就是在说,主线程调用handle_connect()后,handle_connect()会不会立刻return!。

三、什么是阻塞、非阻塞?如何区分?

  1. 阻塞非阻塞说的是什么:阻塞、非阻塞说的是主线程是否等待handle_connect()的return(即主角是调用方,即主线程)
  2. 如何区分阻塞非阻塞:看主线程是否等待调用return后才去做别的事。
    • 阻塞:主线程调用handle()后,等他return了才继续做别的事(不管调用是同步还是异步,只关心函数层面的return)。所以阻塞的主线程更像是对调用的通知+监工,你去把这个事情做一下,然后我看着你做
    • 非阻塞:主线程调用handle()后,不管他有没有return,都直接紧接着做别的事。所以非阻塞的主线程更像是对调用的一种通知,你去把这个事情做一下,然后自己就干别的去了

四、用一个渐进的情景例子来理解同步异步+阻塞非阻塞的各种组合,并分析代码:

  • 情景:有以下3个角色和一个具体的执行任务
    1. 客户端----------------->客户
    2. 服务端主线程----------->包工头
    3. handle_connect()------>具体的项目操作
    4. 服务端子线程----------->包工头手下的工人
  • NOTE:这里多线程时会有点绕,此时要理解为主线程执行的调用是pthread_create():创建一个子线程并让他运行指定函数(招聘一个工人,并告诉它做什么)。

同步阻塞(BIO)

  1. 单线程:公司成立初期,公司里只有包工头一个人。这时候有客户来谈项目(请求连接),包工头接到项目(连接建立)之后,自己动手在主线程内调用handle_connection()把这个项目做完。
    • 同步:handle_connection()只有做完事才会return
    • 阻塞:handle_connection()执行的时候,主线程只能等待。
  2. 多线程:包工头发现这样不行啊,做一个客户项目的时候,其他客户来了没人招待只能干等着,于是他决定外包项目,自己只负责接项目,接到项目后外包给工人去做。
    • 对包工头而言:调用是pthread_creat(),找个临时工过来,告诉他干什么活,然后让他自己去干----->代表让一个工人(子线程)去干活(执行handle),并且我不等你干完,我只是给你分配完这个任务,我就继续去接待其他客户了。
      • 伪异步:因为包工头不再亲手执行handle_connection()了,而是每次接到项目,调用pthread_create()把项目外包给临时工去做。所以此时对主线程而言,调用是pthread_create():创建创建子线程并告诉它干什么,这个调用是会立刻返回的。但对于要处理这个任务而言,任务是未完成的(工人独立的在那做)。即主线程(包公头)执行完调用pthread_create()后,得到的返回值是工人回复他的一个状态(收到),然后工人再去执行具体的任务。
      • 非阻塞:主线程对于这个任务的处理而言肯定是非阻塞的,因为任务还没完成(子线程在那干着呢),包工头已经去接待下一位客户了。但主线程是会阻塞在accept()处的。
    • 对工人而言:调用是handle_connection()
      • 同步:handle_connection()只有做完事才会return
      • 阻塞:handle_connection()执行的时候,子线程只能等待。、
  • 总结:为什么叫伪异步
    1. 主线程的调用会立刻返回一个状态,告诉主线程,好的我知道这个任务了,然后让另一个独立的线程去做。------->所以从主线程的角度来看:调用对于主线程而言是异步的(因为调用,先返回ack状态,再去独立执行任务)
    2. 那为什么是伪呢?因为具体在执行每个任务的子线程都是同步阻塞的,即该模板的底层还是同步阻塞模型。并且主线程虽然不在处理任务处阻塞,但还是会在等待任务时阻塞(与IO多路复用的区别,线程不会阻塞,无论是等待还是读,因为自己不等待,交给select系统调用去做了,并且读就绪的时候,一定是完整的数据,可以直接读完就结束会话)
  1. 线程池:包工头觉得每次有活了再租临时工的话频繁的签约解约太贵了(频繁的创建、销毁线程),所以它决定直接和m个工人签一份按工作时间计算工资的长约(即程序开头创建m个线程)。然后建立一个系统(任务队列),包工头把签到的任务放进去,所有的工人自己无限循环的去查看这个系统,抢任务做。
    • 对包工头而言:调用是enqueue(),把任务放进系统
      • 伪异步:同多线程
      • 非阻塞:同多线程
    • 对工人而言:调用是handle_connect(),执行具体的任务
      • 同步:同多线程
      • 阻塞:同多线程
  • Question:所以个人理解(从线程池和多线程的模型对比中可以看出),只要是Reactor模式都是伪异步,而不是只有线程池+任务队列才是伪异步。
  1. 信号量:包工头觉得如果让工人自己无限循环去抢活干太花钱了,毕竟是按工作时间付费的。所以他决定给工人定个规矩,如果你去系统里找任务时,系统是空的(任务队列中无任务),那你就在系统门口排队睡觉吧(不给你钱了,即工人不占用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()时,文件中的数据是完整的了,线程直接读完就能干别的事了,所以看起来是非阻塞的。(底层其实还是同步阻塞的实现,只不过保证了服务端读时不用等待,因为数据是发完的完整数据,所以看起来非阻塞)。

学些中记录,如果有误还请指出。