打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
【java】同步、异步、阻塞、非阻塞
userphoto

2018.10.08

关注
理解同步与异步
同步是指:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。
异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。
举个例子:普通B/S模式(同步)    AJAX技术(异步)
同步:提交请求->等待服务器处理->处理完毕返回(期间客户端浏览器不能干任何事)
异步: 请求通过事件触发->服务器处理(期间浏览器仍然可以作其他事情)->处理完毕
同步、异步、阻塞和非阻塞(网络编程)
同步
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。
异步
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是其功能已经由异步转化为同步),当一个客户端通过调用 Connect函数发出一个连接请求后,调用者线程立刻可以朝下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候,如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。
同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成。
同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。
异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音(看起来很傻,不是吗)
异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。
也就是说,同步/异步是下载软件的通知方式,或者说 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 请求。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
同步与异步IO、阻塞与非阻塞IO
Nginx 高并发原理解析
Python异步编程和事件驱动
聊聊同步、异步、阻塞与非阻塞
同步(Synchronous)异步(Asynchronous) 阻塞(Blocking)和非阻塞(Non
单线程 多线程 同步 异步 阻塞 非阻塞 理解与总结
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服