Skip to content

Commit 9eedc62

Browse files
committed
modify about_fiber.md
1 parent 45031e5 commit 9eedc62

File tree

1 file changed

+16
-7
lines changed

1 file changed

+16
-7
lines changed

about_fiber.md

+16-7
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ int main(void) {
137137
在介绍了网络协程的基本设计原理后,本章节主要介绍 `libfiber` 网络协程的核心设计要点,为网络协程应用实践化提供了基本的设计思路。
138138
139139
### 3.1、协程调度
140-
libfiber 采用了单线程调度方式,主要是为了避免设计上的复杂度及效率上的影响。如果设计成多线程调度模式,则必须首先需要考虑:
140+
libfiber 采用了单线程调度方式,主要是为了避免设计的复杂性及效率上的影响。如果设计成多线程调度模式,则必须首先需要考虑:
141141
- 多核环境下 CPU 缓存亲和性:因为 CPU 本身有高效的多级缓存,虽然 CPU 多级缓存容量较内存小的多,但访问效率却远高于内存,单线程调度方式下,可以方便编译器有效地进行 CPU 缓存使用优化,使运行指令和共享数据尽可能放置在 CPU 缓存中,而如果采用多线程调度方式,多个线程间共享的数据就可能使 CPU 缓存失效,会造成调度线程越多,协程的运行效率越低的问题;
142142
- 多线程分配任务时的同步问题:当多个线程需要从公共协程任务资源中获取协程任务时,必然需要采用增加『锁』保护机制,一旦产生大量的『锁』冲突,则势必会造成运行性能的严重损耗;
143143
- 事件引擎操作优化:下面会介绍在单线程调度模式下的事件引擎操作优化,在多线程调度则很难进行如此优化。
@@ -154,7 +154,7 @@ libfiber 采用了单线程调度方式,主要是为了避免设计上的复
154154
### 3.2、协程事件引擎设计
155155
#### 3.2.1、跨平台性
156156
libfiber 的事件引擎支持现在主流的操作系统,从而为 libfiber 的跨平台特性提供了有力的支撑,下面为 libfiber 事件引擎所支持的平台:
157-
- **Linux:** sekect/poll/epoll,epoll 为 Linux 内核级事件引擎,采用事件触发机制,不象 select/poll 的轮循方式,所以 epoll 在处理大并发网络连接时运行效率更高;
157+
- **Linux:** sekect/poll/epoll/io_uring,epoll 为 Linux 内核级事件引擎,采用事件触发机制,不象 select/poll 的轮循方式,所以 epoll 在处理大并发网络连接时运行效率更高;而 io_uring 引擎是在内核5.1以后出现的新的事件引擎,由 Facebook 的 Jens Axboe(IO 压测工作 fio 作者) 设计完成,该引擎为真正的异步 IO 模型(为 IO 完成模型),统一了网络 IO 与文件 IO 过程(不象 epoll 仅支持网络,因为其是事件通知方式,无法预知 IO 何时完成)
158158
- **BSD/MacOS:** select/poll/kqueue,kqueue 也为内核级事件引擎,在大并发环境下具有更高的性能;
159159
- **Windows:** select/poll/iocp/Windows 窗口消息,其中 iocp 为 Windows 平台下的内核级高效事件引擎;libfiber 支持采用界面消息引擎做为底层的事件引擎,这样在编写 Windows 界面程序的网络模块时便可以使用协程方式了,之前人们在 Windows 平台编写界面程序的网络模块时,一般采用以下两种方式:
160160
- 采用非阻塞方式,网络模块与界面模块在同一线程中;
@@ -197,7 +197,7 @@ libfiber 的事件引擎支持现在主流的操作系统,从而为 libfiber
197197
- 线程B 中的协程B2 对线程锁2成功加锁;
198198
- 当线程A 中的协程A2 想要对线程锁2 加锁而阻塞时,则会使线程A 的协程调度器阻塞,从而导致整个线程A 中的所有协程被系统挂起;同样,线程B 也会因协程B1 阻塞在线程锁1 上而被阻塞;最终造成了死锁问题。
199199
200-
产生上述死锁的根本原因是单线程调度机制以及操作系统的最小调度单元是线程,系统对于协程是无感知的。因此,在 libfiber 中专门设计了可用于在线程的协程之间使用的事件互斥锁(源码参见 fiber_event.c),其设计原理如下:
200+
产生上述死锁的根本原因是单线程调度机制以及操作系统的最小调度单元是线程,系统对于协程是无感知的。因此,在 libfiber 中专门设计了可用于在线程的协程之间使用的事件互斥锁(源码参见 fiber_event.c, 当前最新的是 fiber_mutex.c,性能更好且占用资源更少),其设计原理如下:
201201
202202
![fiber_event](/img/fiber_event.png)
203203
@@ -213,7 +213,7 @@ libfiber 的事件引擎支持现在主流的操作系统,从而为 libfiber
213213
- 在 Linux 平台上可以使用 eventfd 代替管道,占用资源更少。
214214
215215
#### 3.3.3、协程条件变量
216-
我们在使用线程编程时,都知道线程条件变量的价值:在线程之间传递消息时往往需要组合线程条件变量和线程锁。因此,在 libfiber 中同样设计了协程条件变量(源码见 fiber_cond.c),通过组合使用 libfiber 中的协程事件锁(fiber_event.c)和协程条件变量,用户可以编写出用于在线程之间、线程与协程之间、线程内的协程之间、线程间的协程之间进行消息传递的消息队列。下图为使用 libfiber 中协程条件变量时的交互过程:
216+
我们在使用线程编程时,都知道线程条件变量的价值:在线程之间传递消息时往往需要组合线程条件变量和线程锁。因此,在 libfiber 中同样设计了协程条件变量(源码见 fiber_cond.c),通过组合使用 libfiber 中的协程事件锁(fiber_event.c,目前使用 fiber_mutex.c)和协程条件变量,用户可以编写出用于在线程之间、线程与协程之间、线程内的协程之间、线程间的协程之间进行消息传递的消息队列。下图为使用 libfiber 中协程条件变量时的交互过程:
217217
218218
![fiber_cond](/img/fiber_cond.png)
219219
@@ -227,10 +227,17 @@ libfiber 的事件引擎支持现在主流的操作系统,从而为 libfiber
227227
当有大量协程需要访问后台系统时,通过协程信号量将大量的协程『挡在外面』,只允许部分协程与后端系统建立连接。
228228
**注:** 目前 libfiber 的协程信号量仅用在同一线程内部,还不能跨线程使用,要想在多线程环境中使用,需在每个线程内部为协程创建独立的协程信号量。
229229
230-
### 3.4、域名解析
231-
网络协程库既然面向网络,自然离不开域名的协程化支持,现在很多网络协程库的设计者往往忽视了这一点,有些网络协程库在使用系统 API 进行域名解析时为了防止阻塞协程调度器,将域名解析过程(即调用 gethostbyname/getaddrinfo)扔给独立的线程去执行,当调用系统 API 进行域名解析并发量较大时必然会造成很多线程资源被占用。在 libfiber 中集成了第三方 dns 源码,实现了域名解析过程的协程化,占用更低的系统资源,基本满足了大部分服务端应用系统对于域名解析的需求
230+
### 3.4、协程共享栈
231+
协程网络编程相对于非阻塞网络编程简单太多,大大方便了开发者编写支持高并发的服务程序;但对于有栈协程,意味着每个协程都要占用一段内存用来存放协程栈,所以并发越高,内存占用越多也成为有栈协程的一大弊端,但仔细分析函数压栈出栈及协程挂起唤醒过程,我们会发现在协程挂起时所占用的栈空间大小要远小于协程实际运行过程中占用的最大栈空间大小(考虑到应用业务逻辑的复杂性,可能会存在大量的函数及变量压栈过程,所以占用的栈空间会比较大),而在一个 libfiber 线程空间中只有一个运行栈,所以只需保证一个较大的运行栈空间即可,当协程被挂起时只需将其栈从运行栈中拷贝并保存出来(这个栈空间相对要小一些),而被挂起的协程被唤醒时,只需将其保存的栈拷贝到线程的运行栈上即可。在实践中这的确可以大幅减少高并发时的内存使用,虽然进行栈拷贝时会耗费一些时间,但整体影响并不太大
232232
233-
### 3.5、Hook 系统 API
233+
相对于栈拷贝时的时间损耗,在使用共享栈方式编程时有一点需要特别注意:创建在栈上的变量不能在协程之间或协程与线程之间共享,即是说,一个协程 F1 中的变量 A 传递给另一个协程 F2,并等待 F2 处理后返回,此时的 A 变量不能被创建在 F1 的栈上,因为运行栈在由 F1 切换到 F2 时,变量 A 的地址空间“暂时消失了”,此时变成了 F2 的栈空间,如果该变量在 F2 中继续被使用的话,就会存在地址非法使用的问题;解决变量在协程间共享的方法是将变量创建在堆上(即用 malloc 或 new 创建)。
234+
235+
注:共享栈的想法最初应该是在腾讯的 libco 中提出的,应该也是为了解决大并发时的内存占用问题。
236+
237+
### 3.5、域名解析
238+
网络协程库既然面向网络,自然离不开域名的协程化支持,现在很多网络协程库的设计者往往忽视了这一点,有些网络协程库在使用系统 API 进行域名解析时为了防止阻塞协程调度器,将域名解析过程(即调用 gethostbyname/getaddrinfo)扔给独立的线程去执行,当调用系统 API 进行域名解析并发量较大时必然会造成很多线程资源被占用。在 libfiber 早期通过集成第三方 dns 源码,实现了域名解析过程的协程化,基本满足了大部分服务端应用系统对于域名解析的需求;后来因为跨平台性及代码安全性的需要,在 libfiber 中实现了与域名解析相关的 DNS 协议,替换了第三方 DNS 库。
239+
240+
### 3.6、Hook 系统 API
234241
相对于网络协程的出现时间,很多网络库很早就存在了,并且大部分已有的网络库都是阻塞式的,要改造这些网络库使之协程化的成本是非常巨大的,我们不可能采用协程方式将这些网络库重新实现一遍,目前一个广泛采用的方案是 Hook 与 IO 相关的系统中 API,在 Unix 平台上 Hook 系统 API 相对简单,在初始化时,先加载并保留系统 API 的原始地址,然后编写一个与系统 API 函数名相同且参数也相同的函数,将这段代码与应用代码一起编译,则编译器会优先使用这些 Hook API,下面的代码给出了在 Unix 平台上 Hook 系统 API 的简单示例:
235242
```C
236243
typedef ssize_t (*read_fn)(int, void *, size_t);
@@ -259,6 +266,8 @@ ssize_t read(int fd, void *buf, size_t count) {
259266

260267
通过 Hook API 方式,libfiber 已经可以使 Mysql 客户端库、一些 HTTP 通信库及 Redis 客户端库的网络通信部分协程化,这样在使用网络协程编写服务端应用程序时,大大降低了编程复杂度及改造成本。
261268

269+
为了在 Windows 平台上 Hook IO API,libfiber 集成了微软的一个开源库 detours,但该库是用 C++ 编写的,所以只能集成在 libfiber 的 c++ 模块中,即用户在使用时只能通过使用 libfiber c++ 库达到 Hook 系统 IO API 的目的。
270+
262271
## 四、爱奇艺核心业务的协程实践
263272
### 4.1、CDN 核心模块使用协程
264273
在爱奇艺的自建 CDN 系统中,作为数据回源及本地缓存的核心软件,奇迅承担了重要角色,该模块采用多线程多协程的软件架构设计,如下所示:

0 commit comments

Comments
 (0)