【java】同步、异步、阻塞、非阻塞

理解同步与异步

同步是指:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。 

异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。 

 

举个例子:普通B/S模式(同步)    AJAX技术(异步)

同步:提交请求->等待服务器处理->处理完毕返回(期间客户端浏览器不能干任何事)

异步: 请求通过事件触发->服务器处理(期间浏览器仍然可以作其他事情)->处理完毕

 

 

同步、异步、阻塞和非阻塞(网络编程)

 

同步

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。

按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。

 

异步

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是其功能已经由异步转化为同步),当一个客户端通过调用 Connect函数发出一个连接请求后,调用者线程立刻可以朝下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

 

阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候,如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。

 

非阻塞

非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

对象的阻塞模式和阻塞函数调用

对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。



  1. 同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成。
  2. 同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。
  3. 异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音(看起来很傻,不是吗)
  4. 异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。

也就是说,同步/异步是下载软件的通知方式,或者说 API 被调用者的通知方式。阻塞/非阻塞则是小明的等待方式,或者说 API 调用者的等待方式。

在不同的场景下,同步/异步、阻塞/非阻塞的四种组合都有应用。

同步阻塞

同步阻塞是最简单的方式,就像我们在 C 语言里调用一个函数并等待其返回。

如 stat 系统调用获取文件元数据,只有同步阻塞一种模式。我在访问量很大的一个文件服务器(mirrors.ustc.edu.cn)上遇到过大量 nginx 进程处于 D(uninterruptible)状态的问题,就是因为 stat 系统调用不提供非阻塞 I/O(O_NONBLOCK)选项(nginx 在能用非阻塞 I/O 的地方都用了非阻塞)。文件的元数据被从磁盘中读入进来的时间里,这个 nginx worker 进程只能在内核态苦苦等待而无法做其他事。不提供 O_NONBLOCK 选项,对内核开发者来说这是省事了,但对用户来说就要付出性能的代价了。

同步非阻塞

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。

 

同步非阻塞方式相比同步阻塞方式:

  • 优点是能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
  • 缺点是任务完成的响应延迟增大了,因为每过一段时间才去轮询一次,而任务可能在两次轮询之间的任意时间完成。

由于同步非阻塞方式需要不断轮询,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。这就是所谓的 “I/O 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。Windows 下则有 WaitForMultipleObjects 和 IO Completion Ports API 与之对应(Windows API 的命名简直甩 POSIX API 几条街有木有!)

Linux I/O 多路复用

高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是可以同时工作的物理资源数量(如 CPU 核数)。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 I/O 请求丢到后台去,这就可以在一个进程里服务大量的并发 I/O 请求。

异步非阻塞

异步非阻塞,就是把一件事丢到 “后台” 去做,完成之后再通知。

在 Linux 中,通知的方式是 “信号”。

  • 如果这个进程正在用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
  • 如果这个进程正在内核态忙着做别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
  • 如果这个进程现在被挂起了,例如无事可做 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。

异步 API 说来轻巧,做来难,这主要是对 API 的实现者而言的。Linux 的异步 I/O(AIO)支持是 2.6.22 才引入的,还有很多系统调用不支持异步 I/O。Linux 的异步 I/O 最初是为数据库设计的,因此通过异步 I/O 的读写操作不会被缓存或缓冲,这就无法利用操作系统的缓存与缓冲机制。

Windows API 里的异步 I/O API(被称为 Overlapped I/O)则优雅得多,可以在 ReadFileEx、WriteFileEx 等 I/O API 上指定回调函数,当 I/O 操作完成时就会调用它。这相当于在 “信号” 的基础上提供了一层封装。除了指定回调函数,这些异步 I/O 请求还可以使用 “传统” 的同步阻塞方式(WaitForSingleObject)、多路复用的同步非阻塞方式(WaitForMultipleObjects)来等待。多个异步 I/O 请求也可以绑定到一个 I/O Completion Port 上一起等待。

Windows 异步 I/O 原理

很多人把 Linux 的 O_NONBLOCK 认为是异步方式,但事实上这是前面讲的同步非阻塞方式。由于 Linux 的异步 I/O 难用,nginx 早期版本一直使用的是 O_NONBLOCK 和 epoll,从 0.8.11 开始支持异步 I/O,但默认使用的仍然是同步非阻塞方式。需要指出的是,虽然 Linux 上的 I/O API 略显粗糙,但每种编程框架都有封装好的异步 I/O 实现。操作系统少做事,把更多的自由留给用户,正是 UNIX 的设计哲学,也是 Linux 上编程框架百花齐放的一个原因。

异步阻塞

都有下载完成通知了,我还傻傻地盯着进度条干什么?这种看起来很傻的方式也是有用的。有时我们的 API 只提供异步通知方式,例如在 node.js 里,但业务逻辑需要的是做完一件事后做另一件事,例如数据库连接初始化后才能开始接受用户的 HTTP 请求。这样的业务逻辑就需要调用者是以阻塞方式来工作。

为了在异步环境里模拟 “顺序执行” 的效果,就需要把同步代码转换成异步形式,这称为 CPS(Continuation Passing Style)变换。BYVoid 大神的 continuation.js 库就是一个 CPS 变换的工具。用户只需用比较符合人类常理的同步方式书写代码,CPS 变换器会把它转换成层层嵌套的异步回调形式。

CPS 变换后的异步代码示例(来源:continuation.js)

 

用户手写的同步代码示例(来源:continuation.js)

另外一种使用阻塞方式的理由是降低响应延迟。如果采用非阻塞方式,一个任务 A 被提交到后台,就开始做另一件事 B,但 B 还没做完,A 就完成了,这时要想让 A 的完成事件被尽快处理(比如 A 是个紧急事务),要么丢弃做到一半的 B,要么保存 B 的中间状态并切换回 A,任务的切换是需要时间的(不管是从磁盘载入到内存,还是从内存载入到高速缓存),这势必降低 A 的响应速度。因此,对实时系统或者延迟敏感的事务,有时采用阻塞方式比非阻塞方式更好。

最后补充一句,同步/异步的概念在不同语境下是不同的,本文说的是 API 或者 I/O。在其他语境里可能是别的意思,例如分布式系统里的同步表示是各节点按照时钟节拍同步,而异步是收到消息后立即执行。

非阻塞的应用场景主要是 apache、nginx 等需要高并发的程序,以便同时处理多个请求。(这个忘了说了,我补充到原文去)

同步非阻塞和异步非阻塞主要取决于 API 提供的接口是同步的还是异步的。

高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是可以同时工作的物理资源数量(如 CPU 核数)。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 I/O 请求丢到后台去,这就可以在一个进程里服务大量的并发 I/O 请求。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值