“异步”这个词大概是15年前后日常出现在耳边的, 当时nodejs如日中天, 吹的就是一个异步与高并发. 虽然在之前已经有各种各样的异步lib了, 但感谢nodejs, 把async
在中国带火
先背一遍概念, nodejs使用的libuv封装了eventloop, 异步相关的操作放在loop中进行. eventloop底层实现根据平台调用不同的api, linux的epoll, windows的iocp, 以及bsd的kqueue.
一个典型的event loop实现大概是
1 2 3 4 5
| while (true) { get_events();
handle_events(); }
|
你说这个太简单了? 我们来看看真实的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int uv_run(uv_loop_t* loop, uv_run_mode mode) { ...
while (r != 0 && loop->stop_flag == 0) { ...
uv__run_pending(loop); uv__run_idle(loop); uv__run_prepare(loop); ...
uv__io_poll(loop, timeout);
for (r = 0; r < 8 && !QUEUE_EMPTY(&loop->pending_queue); r++) uv__run_pending(loop); ... }
... }
|
再看看uv__io_poll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void uv__io_poll(uv_loop_t* loop, int timeout) { ...
for (;;) { nfds = poll(loop->poll_fds, (nfds_t)loop->poll_fds_used, timeout); ...
for (i = 0; i < loop->poll_fds_used; i++) { ... pe->revents &= w->pevents | POLLERR | POLLHUP;
if (pe->revents != 0) { if (w == &loop->signal_io_watcher) { have_signals = 1; } else { uv__metrics_update_idle_time(loop); w->cb(loop, w, pe->revents); }
nevents++; } } ... } }
|
可以看到, 抛开各种细节不谈, 整体的框架符合上面的模型,
再看看chenshuo的教学框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void EventLoop::loop() { ... while (!quit_) { activeChannels_.clear(); pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_); ++iteration_; if (Logger::logLevel() <= Logger::TRACE) { printActiveChannels(); } eventHandling_ = true; for (Channel* channel : activeChannels_) { currentActiveChannel_ = channel; currentActiveChannel_->handleEvent(pollReturnTime_); } currentActiveChannel_ = NULL; eventHandling_ = false; doPendingFunctors(); }
LOG_TRACE << "EventLoop " << this << " stop looping"; looping_ = false; }
|
不愧是教科书, 与我们模型如出一辙 因为我就是看这本书学的
看到这里你可能想问, 那么, 这异步到底异在哪里, 不管get_events
还是handle_events
不都是同步的函数吗
确实如此, 通常我们实现异步, 理所应当的会想到Queue: 当要执行某个操作的时候, 不是直接执行, 而是塞到队列里, 在另一个处从队列里取出再执行. 实际上epoll也是这么做的, 只不过Queue存在于内核中, 而我们的get_events
便是将内核队列里的events取出.
这时候你可能又会问, 我知道epoll怎么用了, 那么这和你文章tag里的c# io_uring有什么关系呢.
好吧, 废话完了, 我们进入正题.
众所周知, C#是最早引入async/await
来实现异步调用的语言之一. 注意到我没有说async/await关键字
, 因为这俩加入的太晚了, 如果作为关键字会造成大量legecy代码broken, 毕竟不能让变量名和关键字重名是大部分编程语言的共识. 在现在版本的C#中你仍然可以自定义一个叫async
的变量. 同时, 在CLR中, 也没有async/await
的指令, 编译器会将其编译成StateMachine来执行. 同时你还可以自己实现TaskScheduler来自定义对Task
的调度.
背完了概念, 那么, 他的底层实现也是event loop吗. 是的, 没错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private void EventLoop() { try { SocketEventHandler handler = new SocketEventHandler(this); while (true) { int numEvents = EventBufferCount; Interop.Error err = Interop.Sys.WaitForSocketEvents(_port, handler.Buffer, &numEvents); ...
if (handler.HandleSocketEvents(numEvents)) { ScheduleToProcessEvents(); } } } catch (Exception e) { Environment.FailFast("Exception thrown from SocketAsyncEngine event loop: " + e.ToString(), e); } }
|
注释也提到底层的实现是epoll/kqueue
:
// The responsibility of SocketAsyncEngine is to get notifications from epoll|kqueue
…
好, 这下C#的实现我们可以假装搞明白了, 同时我们知道linux 5.1以后提供了io_uring, 那么io_uring也符合最上面给出的模型吗?
首先由于io_uring的用法过于复杂, 作者亲自开发了一个库来简化, 虽然还是很复杂.
为了方便演示, 我们来用C#调用liburing, 写一个简单的tcp echo server.
先把liburing头文件里inline的函数导出一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #define IOURINGINLINE const #include <liburing.h> #ifdef __cplusplus extern "C" { #endif
#include <setup.c> #include <queue.c> #include <register.c> #include <syscall.c> #include <version.c>
#ifdef __cplusplus } #endif
|
然后引入Tmds.Libc来调用system api, 再自己导入几个liburing的, e.g.
1 2 3 4 5
| [DllImport(LibUringReExportSo, SetLastError = true)] public static unsafe extern int io_uring_queue_init(uint entries, io_uring* ring, uint flags);
[DllImport(LibUringReExportSo, SetLastError = true)] public static unsafe extern int io_uring_wait_cqe(io_uring* ring, io_uring_cqe** cqe_ptr);
|
然后就可以愉快的写C#了, 基本原理是从这里抄过来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public unsafe void ServerLoop(int serverSocket, io_uring* ring) { ... AddAcceptRequest(serverSocket, &clientAddr, &len, ring); while (true) { var ret = io_uring_wait_cqe(ring, &cqe); var req = (Request*)cqe->user_data; switch (req->event_type) { case EventType.EVENT_TYPE_ACCEPT: ... case EventType.EVENT_TYPE_READ: ... case EventType.EVENT_TYPE_WRITE: ... } io_uring_cqe_seen(ring, cqe); } }
|
可以发现, 仍然是上面这一套框架, 甚至这一套自定义event看起来比epoll更加清晰.
那么, 既然epoll和uring在用法上大同小异, 肯定有人想在成熟的框架上加上io_uring的支持, libuv有, dotnet也有. 如果本文的用法能对集成liburing进入dotnet有所启发就再好不过了做梦.jpg
以上, 完整demo在这里, 但是写得非常难看, 而且各种泄露都没修, 能跑就行.
本文链接: https://blog.fallenwood.net/2023/04/07/csharp_uring/