(文件或者 socket)IO 分三种:
GRPC 在 Windows 上发包的时候是先尝试 2,如果不行就做 3,以减少线程切换次数。
什么叫异步?你把一个函数的 parameter 都在堆分配而不是在栈上分配,然后同时给它一个 callback,让它完成后通知你。
这样做的好处是内核可以自由的去分配 CPU,让多个 CPU 同时处理这些事件,而不是把 event loop 压在单个 cpu 上。
但是有个难处你得想想:那些 input parameter 什么时候释放?是在被调用函数真正执行完才能释放。举个例子,如果你要往 socket 写东西,那么必须等收到 write 函数的 callback 后你才能释放那个 write buffer 。所以必须要有额外的 buffer 管理机制,否则内存很容易就爆掉了。
对于 java 用户来说,如果你用原生的 jdk,那么可以在 Windows 上用异步 io 。如果你用其它任何框架,那么就没戏。netty 曾经试图支持异步 io,后来放弃了。
现代的观点是 io 和线程池应该合在一起,统一交给操作系统来管理。java 标准库中根本就没有内核态的线程池,所以这条路走不下去。
什么叫合在一起管理:试想一下,假如你现在提交了一个异步 IO 任务,现在它执行完了,那么去哪执行 callback 函数呢?理想情况是你有一个线程池在做这件事情,并且这个线程池的活跃的线程数量不大于总逻辑 CPU 数量。如果你能做到让所有的代码都是非阻塞的,甚至连个条件变量都没有,也没有任何的 nested parallelism 。那么这条路是可行的。
如果线程池里有阻塞性的 io 会怎样: 举个例子,假设你用 windows iocp 写了一个 HTTP server 。然后来了一个 http 请求访问 index.html 。这个请求被分配给 server 的某个线程。然后它一看 index.html 不在内存里,就发起了一个同步的 io 请求去硬盘上读。同时该线程被挂起,从而不计入活跃线程数量。接着又来了第二个 http 请求,同样。然后来了 1000 个,都是这样,都被挂起。然后硬盘把数据给我们读上了!于是这 1000 个线程同时回到 RUNNABLE 的状态,于是 CPU 傻掉了。所谓的让内核统一管理 IO 和线程池就是为了解决这样的问题。
但是缺点是如果线程池在内核态,那么每次调度的开销就很大,导致它不适合轻量级的计算任务(如矩阵乘法)。
另外说一句:fixed size 的 thread pool 的时代已经过去了。
当下的热点是什么? work-stealing,多任务队列。但是这些模型都不适合 IO 。
1
Goldilocks OP 哦,对了,还有很有趣的一件事情:
如果异步 io 立即返回了怎么办? 为了性能考虑,框架会允许直接在发起 IO 请求的线程上处理 callback 。比如 java 傻乎乎的还是会调用 callback 函数。但是 callback 里会再次做类似的事情。所以如果没有特殊的处理。java 的栈直接就爆掉了。 Windows 从 vista 开始加了一个 API 叫 SetFileCompletionNotificationModes,如果你把它设置成 FILE_SKIP_COMPLETION_PORT_ON_SUCCESS,那就不会执行 callback 。这就比 java 的那套设计合理多了。java 必须借助 JVM 的帮助来防止函数过度嵌套而导致 stack overflow 。 |
2
YouLMAO 2021-01-06 14:51:12 +08:00
在 io 线程做业务线程的逻辑, 说到底, 还是同步思维
现代企业大部分都是 io 密集的微服务, 当中大部分是 network io+disk io 多看看 Apache 顶级项目, 业务线程是全异步的, 每个请求不绑定固定业务线程的, 通过状态机全异步, 比如业务线程 7, req1 需要发 rpc 查账户状态和查余额, 发出 req1a, req1b, 发完 req1a, req1b 这个 req1 就挂起了, 但业务线程 7 继续处理 req23, 当 req1a, 1b 有返回值 or io timeout, req1 又被激活了, 任意业务线程继续处理 req1 |
3
lix7 2021-01-06 14:53:31 +08:00
Linux 再等几年主流发行版 Kernel 升到 5.1+支持 io_uring 也就好了
|
4
Goldilocks OP @YouLMAO 大侠你说的对
|