异步编程
2022-09-29 08:55:22 0 举报
AI智能生成
异步编程内容,跨越底层内核到CPU
作者其他创作
大纲/内容
1.时代背景:现有的伪同步执行
软硬中断
CPU层/软件层抢占式调度
CPU流水线机制
2.巨人镰刀:系统调用(syscall)
定义:操作系统提供的公共API,提供与内核通信的能力
特点1:由于是跨空间(用户态<->内核态)通信,开销比纯用户态通信大
特点2:Windows与UNIX或类UNIX系统提供的API可能较大差异,一些跨<br>平台应用框架内部实现了与不同平台的API对接
题外话:一般Linux和MacOS有更简单的API,需要编写的代码量<br>会少一些,通常(但不总是)完全相同的API在这两个系统下都是<br>有效的。而Windows相对更加复杂,需要你去创建更多的结构体<br>来传递信息(而不是直接使用内建的原生类型)
3.性能之巅:汇编(assembly)直接调用CPU的API
特点1:由于是基于CPU接口编程,相比于我们常见的<br>基于操作系统接口编程,能做的事情更多(更"肆无忌惮")
了解:大部分时候我们并不需要基于CPU接口编程,除非<br>你从事的编写驱动类似的相关项目(这需要熟悉CPU相关<br>的硬件基础知识)
4.接近真相:常见的I/O处理策略
1.使用一个系统线程处理一个I/O事件
优点:易理解、易编程、可并行
缺点:
1.单个系统线程(堆栈)占用内存不小,无法处理高并发I/O事件
2.涉及系统调用多,开销大,造成延迟高
3.系统线程切换慢
4.用户无法指定线程优先级,仅由OS调度
2.使用一个绿色线程处理一个I/O事件
优点:易编程、性能良好、可以指定线程优先级
缺点:
1.需要在用户态运行时构建一个绿色线程机制(实现成本较大)
2.基于自建的线程运行不够灵活
案例:Goroutine、Python(greenThread)
3.<b>【最优】</b>使用OS内核提供的EventLoop处理所有I/O事件
优点:资源利用率最高、高效率
缺点:
1.需要兼容不同OS的eventLoop实现,一次性且较大实现成本
案例:actix(Rust)、gnet(Golang)、netty(Java)、uvloop(Python),——很多是基于C写的libuv库
5.整装待发:通过syscall使用OS支持的EventLoop
不同OS对EventLoop实现方式大同小异
Windows——IOCP(I/O Complete Ports)
*unix——Epoll
*BSD——Kqueue
三种实现的大致读取socket逻辑
Epoll&Kqueue设计相似:基于已准备事件的eventloop
1.调用名为<font color="#d32f2f" face="Comic Sans MS" style="">epoll_create</font>和<font face="Comic Sans MS" color="#d32f2f">kqueue</font>的<u>syscall</u>,创建一个eventloop
2.调用名为<font face="Comic Sans MS" color="#d32f2f">socket</font>的syscall向OS申请一个socket的FD(文件描述符)
3.调用名为<font face="Comic Sans MS" color="#d32f2f">epoll_ctl</font>和<font face="Comic Sans MS" color="#d32f2f">kevent</font>的syscall,注册该socket的Read事件(以便在可读时收到OS的通知)
4.调用名为<font face="Comic Sans MS" color="#d32f2f">epoll_wait</font>或<font face="Comic Sans MS" color="#d32f2f">kevent</font>的syscall,等待来自OS的事件通知,会阻塞当前线程
5.收到通知时,根据事件类型作不同处理,一般有以下几种事件:<br> a.某socket的close事件,一般调用<font color="#1976d2" face="Comic Sans MS">syscall.Close()</font>关闭fd<br> b.client的连接请求,一般调用<font color="#1976d2" face="Comic Sans MS">syscall.Accept()</font><font face="Comic Sans MS" color="#000000">创建并</font>维护新的socket<br> c.某socket收到数据,且已准备好被读取,一般调用<font color="#1976d2" face="Comic Sans MS">syscall.Read()</font>读取数据<br>
IOCP:基于已完成事件的eventloop(<u>待优化</u>)
1.调用名为<font face="Comic Sans MS" color="#d32f2f">CreateIoCompletionPort()</font>的syscall,创建一个eventloop,<br>得到一个<b>root_CP</b>对象(complete port),这个root_CP对象是一个<br>专职的、负责所有其他new conn的CP对象的I/O完成事件的接收和分发处理
2.启动指定数量个系统线程(一般等于CPU核数),用于处理不同I/O完成事件,<br>如<font color="#2196f3"><b>读完成/写完成</b></font>)线程内部逻辑:<br>
2.1 启动一个while死循环,<u>下面是循环内的逻辑</u>
2.2 初始化一些马上会用到的对象,如一个socket句柄—sock,<br>一个CP对象—hComPort、已传输bytes长度变量—bytesTransfered、<br>包含数据的I/O对象—ioInfo
2.3 阻塞调用<font color="#d32f2f" face="Comic Sans MS">GetQueuedCompletionStatus</font><font color="#d32f2f">(参数为刚<br>初始化的各对象指针)</font>,该调用会在有一个I/O完成事件发生时返回,<br>返回时相关数据会写入上述各指针对象
2.4 当OS回调上述函数时,判断若是读完成事件:<br> a.若bytesTransfered=0,表示传输EOF,需<font color="#d32f2f" face="Comic Sans MS">closesocket</font><font color="#d32f2f">()</font><br> b.若不为0,则从ioInfo<font color="#1976d2"><b>直接取出数据(此时数据已从内核态copy到用户态,<br> 无需再执行系统调用读取数据)</b></font>,并作相应处理,最后执行<br> 系统调用<font color="#d32f2f" face="Comic Sans MS">WSASend</font><font color="#d32f2f">()</font>往socket写入数据;(<i>WSASend也是非阻塞调用</i>)
注:IOCP是在数据拷贝完成后通知我们,<br><b style="">区别于</b>Epoll&Kqueue在数据准备好拷贝时通知我们
2.5 若是(server自己的)写完成事件,一般没有特别处理,可以简单记录log
2.6 再次执行系统调用<font color="#d32f2f" face="Comic Sans MS">WSARecv</font><font face="Comic Sans MS" color="#d32f2f">(sock, &ioInfo...)</font>继续等待client发送数据
<font color="#9e9e9e">2.7 重复while循环</font>
3.执行系统调用<font face="Comic Sans MS" color="#d32f2f">WSASocket()</font>申请一个socket句柄—root_sock,并<font color="#d32f2f" face="Comic Sans MS">bind()</font>地址<br>然后开始<font color="#d32f2f" face="Comic Sans MS">Listen(root_sock); </font><font face="Comic Sans MS" color="#212121"><i>Listen是非阻塞调用</i></font>
4.在主线程中启动一个while死循环,循环内部逻辑:
4.1 阻塞系统调用<font face="Comic Sans MS" color="#d32f2f">accept</font><font color="#d32f2f">()</font>,等待client连接
4.2 当有client连接时,accept()返回一个socket句柄—new_sock,<br><span style="font-size: inherit;">并将其作为参数再次调用</span><font color="#d32f2f" face="Comic Sans MS" style="font-size: inherit;">CreateIoCompletionPort(new_sock, root_CP...)</font>
4.3 初始化一个PER_IO_DATA类型的ioInfo对象
4.4 将new_sock和ioInfo对象作为参数执行系统调用<font face="Comic Sans MS" color="#d32f2f">WSARecv</font><font face="Comic Sans MS" color="#d32f2f">(new_sock, &ioInfo...)</font>,<br>此处为<i>非阻塞调用;(注:收到读完成事件的处理逻辑在第二步启动的线程内完成)</i>
<font color="#9e9e9e">4.5 重复while循环</font>
6.上阵冲锋:Node.js
1.缘起:作者的初衷是创建一个便于实现高性能的web服务器的服务端框架
2.设计:需要一个低成本、高效率实现能够支撑高并发I/O请求的模型
3.定稿:采用操作系统支持的基于事件循环的单线程非阻塞I/O模型(<b>调用libuv</b>)<br>并使用C++在V8引擎之上构建服务端的JS运行时
4.选择载体语言JS,理由是
JS是高级语言,具备其他低级语言不支持的特性<br>如闭包、函数式编程、语法简洁性
同事件循环模型核心一致,JS也是单线程运行和事件驱动编程,天生契合
JS本是前端语言,若能应用到服务端,则可以重用部分代码;<br>并在项目中统一前后端语言
Chrome提供的V8引擎能够直接解释(编译)JS为机器码,大大提高运行性能
JS的拥趸者本来就多
5.内部任务处理方式
I/O密集型任务,由跨平台的<b><font face="Comic Sans MS">libuv库</font></b>处理
CPU密集型任务,由线程池处理(Node的C++扩展都是使用线程池执行)
0 条评论
下一页