|
12 | 12 | - [协程窃取](#协程窃取)
|
13 | 13 | - [调度器](#调度器)
|
14 | 14 | - [抢占调度](#抢占调度)
|
| 15 | +- [协程池](#协程池) |
| 16 | +- [EventLoop](#EventLoop) |
15 | 17 |
|
16 | 18 | ## 诞生之因
|
17 | 19 |
|
| 20 | +在早期程序员为了支持多个用户并发访问服务应用,往往采用多进程方式,即针对每一个TCP网络连接创建一个服务进程。在2000年左右,比较流行使用CGI方式编写Web服务,当时人们用的比较多的Web服务器是基于多进程模式开发的Apache |
| 21 | +1.3.x系列,因为进程占用系统资源较多,而线程占用的资源更少,所以人们开始使用多线程方式编写Web服务应用,这使单台服务器支撑的用户并发度提高了,但依然存在资源浪费的问题。 |
| 22 | + |
18 | 23 | 2020年我入职W公司,由于内部系统不时出现线程池打满的情况,再加上TL读过[《Java线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)
|
19 | 24 | ,我们决定构建自己的动态线程池,从结果来看,效果不错:
|
20 | 25 |
|
@@ -138,7 +143,8 @@ PS:这里解释下hook技术,简单的说,就是函数调用的代理,
|
138 | 143 |
|
139 | 144 | 暂时采用[corosensei](https://github.com/Amanieu/corosensei),目前正在尝试自研无栈协程。
|
140 | 145 |
|
141 |
| -`suspend`和`resume`原语直接复用[corosensei](https://github.com/Amanieu/corosensei),这里不过多赘述。 选好底层库,接着就是确定协程的状态了,下面是个人理解: |
| 146 | +`suspend`和`resume`原语直接复用[corosensei](https://github.com/Amanieu/corosensei),这里不过多赘述。 |
| 147 | +选好底层库,接着就是确定协程的状态了,下面是个人理解: |
142 | 148 |
|
143 | 149 | <div style="text-align: center;">
|
144 | 150 | <img src="img/state.png" width="50%">
|
@@ -192,37 +198,44 @@ RingBuffer作为最常用的高性能数据结构,主要有几个优点:
|
192 | 198 |
|
193 | 199 | 无非两种方案,一是先从共享队列取协程,再从其他本地RingBuffer取协程;二是先从其他本地RingBuffer取协程,再从共享队列取协程。怎么选?竞争共享队列等价于竞争一把共享写锁,再竞争其他本地RingBuffer等价于竞争多把共享写锁,从并发冲突的角度考虑,自然是资源越多越好,因此选择方案二。
|
194 | 200 |
|
195 |
| -其他实现细节可参考[《Tokio解析之任务调度》](https://baijiahao.baidu.com/s?id=1746023143258422548),虽然实际用的是[st3](https://github.com/asynchronics/st3),但原理相通。 |
| 201 | +其他实现细节可参考[《Tokio解析之任务调度》](https://baijiahao.baidu.com/s?id=1746023143258422548) |
| 202 | +,虽然实际用的是[st3](https://github.com/asynchronics/st3),但原理相通。 |
196 | 203 |
|
197 | 204 | ## 调度器
|
198 | 205 |
|
199 | 206 | 还记得[底层抽象](#底层抽象)里提到的协程状态吗?
|
200 | 207 |
|
201 |
| -我们用[时间轮](#时间轮)来实现suspend队列,基于[协程窃取](#协程窃取)实现ready队列(至于syscall集合,先卖个关子),剩下只要实现submit(往ready队列添加协程)和try_schedule(非阻塞地调度协程)两个方法,就完成了一个功能强大的调度器。 |
| 208 | +我们用[时间轮](#时间轮)来实现suspend队列,基于[协程窃取](#协程窃取)实现ready队列(至于syscall集合,先卖个关子) |
| 209 | +,剩下只要实现submit(往ready队列添加协程)和try_schedule(非阻塞地调度协程)两个方法,就完成了一个功能强大的调度器。 |
202 | 210 |
|
203 | 211 | <div style="text-align: center;">
|
204 |
| - <img src="img/scheduler.png" width="75%"> |
| 212 | + <img src="img/scheduler.png" width="80%"> |
205 | 213 | </div>
|
206 | 214 |
|
207 |
| -submit方法的实现非常简单,就不阐述了。我们直接谈try_schedule,其实也简单,就是真正调度前,检查一下suspend队列是否有需要运行的协程,如果有则把它加到ready队列,然后调度ready队列的协程就行了(任务窃取算法在[底层](#协程窃取)已经实现了)。 |
| 215 | +submit方法的实现非常简单,就不阐述了。我们直接谈try_schedule,其实也简单,就是真正调度前,检查一下suspend队列是否有需要运行的协程,如果有则把它加到ready队列,然后调度ready队列的协程就行了( |
| 216 | +任务窃取算法在[底层](#协程窃取)已经实现了)。 |
208 | 217 |
|
209 | 218 | 另外,从扩展性的角度考虑,作者添加了Listener API,当协程创建/挂起/陷入系统调用/完成时,均会回调用户函数,典型适用场景如打日志、监控等等。
|
210 | 219 |
|
211 | 220 | ## 抢占调度
|
212 | 221 |
|
213 | 222 | 抢占调度可以让一个正在执行的协程被中断,以便其他等待执行的协程有机会被调度并运行。这种机制可以在遇到阻塞操作或计算密集型任务时及时切换执行其他协程,避免因为一个协程的长时间执行而导致整个程序的性能下降。
|
214 | 223 |
|
215 |
| -在go语言中,抢占调度是通过采用协作式和抢占式混合调度实现的。当协程主动发起I/O操作、调用runtime.Gosched()函数或访问channel等等时,会发生协作式调度,即主动让出CPU并让其他协程执行。而当一个协程超过一定时间限制或发生系统调用等情况时,会发生抢占式调度,即强制剥夺当前协程的执行权。这样的混合调度机制可以在保证程序的高并发性的同时,增加系统的响应能力。 |
| 224 | +在go语言中,抢占调度是通过采用协作式和抢占式混合调度实现的。当协程主动发起I/O操作、调用runtime.Gosched() |
| 225 | +函数或访问channel等等时,会发生协作式调度,即主动让出CPU并让其他协程执行。而当一个协程超过一定时间限制或发生系统调用等情况时,会发生抢占式调度,即强制剥夺当前协程的执行权。这样的混合调度机制可以在保证程序的高并发性的同时,增加系统的响应能力。 |
216 | 226 |
|
217 |
| -为了提高程序的并发性和响应能力,open-coroutine也引入了基于信号的抢占调度机制。与goroutine略微有些差异的是,当发生系统调用时,部分系统调用也会发生协作式调度(先卖个关子,后续再详细介绍)。 |
| 227 | +为了提高程序的并发性和响应能力,open-coroutine也引入了基于信号的抢占调度机制。与goroutine略微有些差异的是,当发生系统调用时,部分系统调用也会发生协作式调度( |
| 228 | +先卖个关子,后续再详细介绍)。 |
218 | 229 |
|
219 | 230 | 我们把以下代码当成协程体:
|
220 | 231 |
|
221 | 232 | ```c++
|
222 |
| -// 模拟死循环协程体 |
223 |
| -while (count < 1) { |
224 |
| - std::cout << "Waiting for signal..." << std::endl; |
225 |
| - sleep(1); |
| 233 | +{ |
| 234 | + // 模拟死循环协程体 |
| 235 | + while (count < 1) { |
| 236 | + std::cout << "Waiting for signal..." << std::endl; |
| 237 | + sleep(1); |
| 238 | + } |
226 | 239 | }
|
227 | 240 | ```
|
228 | 241 |
|
@@ -278,7 +291,10 @@ Received signal 2
|
278 | 291 | thread main finished!
|
279 | 292 | ```
|
280 | 293 |
|
281 |
| -解释下,在主线程中,我们开启了一个子线程t1,在注册信号处理函数后,子线程t1将会陷入死循环并输出`Waiting for signal...`到控制台。主线程在睡眠1s后,向子线程t1发送信号,子线程t1的执行权被移交给信号处理函数signal_handler,信号处理函数结束后,子线程t1的执行权回到之前执行的地方(也就是`std::cout << "Waiting for signal..." << std::endl;`下面一行代码)继续执行,此时条件不满足,子线程t1跳出循环,打印`thread main finished!`,子线程t1执行完毕,随后主线程结束等待,也执行完毕。 |
| 294 | +解释下,在主线程中,我们开启了一个子线程t1,在注册信号处理函数后,子线程t1将会陷入死循环并输出`Waiting for signal...` |
| 295 | +到控制台。主线程在睡眠1s后,向子线程t1发送信号,子线程t1的执行权被移交给信号处理函数signal_handler,信号处理函数结束后,子线程t1的执行权回到之前执行的地方( |
| 296 | +也就是`std::cout << "Waiting for signal..." << std::endl;`下面一行代码) |
| 297 | +继续执行,此时条件不满足,子线程t1跳出循环,打印`thread main finished!`,子线程t1执行完毕,随后主线程结束等待,也执行完毕。 |
282 | 298 |
|
283 | 299 | 接下来,我们考虑更复杂的情况,即需要重复抢占,修改代码如下:
|
284 | 300 |
|
@@ -377,16 +393,37 @@ Waiting for signal...
|
377 | 393 | thread main finished!
|
378 | 394 | ```
|
379 | 395 |
|
380 |
| -上述涉及的系统调用sigemptyset、sigaction、pthread_kill、pthread_sigmask和sigdelset,建议阅读《Linux/UNIX系统编程手册》20~22章节的内容以加深理解。 |
| 396 | +上述涉及的系统调用sigemptyset、sigaction、pthread_kill、pthread_sigmask和sigdelset,建议阅读《Linux/UNIX系统编程手册》20~ |
| 397 | +22章节的内容以加深理解。 |
381 | 398 |
|
382 |
| -## EventLoop |
| 399 | +## 协程池 |
383 | 400 |
|
384 |
| -## JoinHandle |
| 401 | +虽然协程比线程耗费的资源更少,但频繁创建和销毁协程仍然会消耗大量的系统资源,因此将协程池化是必须的。池化后,能带来几个显著优势: |
385 | 402 |
|
386 |
| -## Hook |
| 403 | +1. 资源管理:协程池可以管理协程的创建、销毁和复用。通过使用协程池,可以事先创建好一定数量的协程,并将它们存储在池中供需要时使用。这样可以避免频繁的创建和销毁协程,提高系统的资源利用率。 |
| 404 | + |
| 405 | +2. 避免协程饥饿:在使用协程池时,协程会被持续提供任务,避免了协程执行完任务后处于空闲状态的情况。 |
| 406 | + |
| 407 | +3. 控制并发度:协程池可以限制并发协程的数量,避免系统因为协程过多而过载。通过设置协程池的大小,可以控制并发度,保证系统资源的合理分配。 |
387 | 408 |
|
388 |
| -## 再次封装 |
| 409 | +4. 提高代码的可维护性:使用协程池可以将任务的执行和协程的管理分离开来,使代码更加清晰和可维护。任务的执行逻辑可以集中在任务本身,而协程的创建和管理则由协程池来负责。 |
389 | 410 |
|
390 |
| -## 极简属性宏 |
| 411 | +在open-coroutine中,协程池是惰性的,如果用户不主动调度,任务将不会执行,具体请看下方的流程图: |
391 | 412 |
|
392 |
| -## 发布之后 |
| 413 | +<div style="text-align: center;"> |
| 414 | + <img src="img/pool.png" width="100%"> |
| 415 | +</div> |
| 416 | + |
| 417 | +## EventLoop |
| 418 | + |
| 419 | +传统多进程或多线程编程方式均采用了阻塞编程,这会使得服务端的进程或线程因等待客户端的请求数据而变得空闲,而且在该空闲期间还不能做别的事情,白白浪费了操作系统的调度时间和内存资源。这种一对一的服务方式在广域网的环境下显示变得不够廉价,于是人们开始采用非阻塞网络编程方式来提升网络服务并发度。 |
| 420 | + |
| 421 | +event loop核心采用非阻塞IO和事件队列技术,是一种常见的异步编程模型,可以高效地处理多个并发任务。虽然自身为单线程模型,但可轻易通过多线程扩展来提升程序性能。 |
| 422 | + |
| 423 | +跨平台方面,目前open-coroutine仅从[mio](https://github.com/tokio-rs/mio) |
| 424 | +移植了epoll和kevent,意味着在windows下无法使用;具体操作层面,提供对读写事件的添加/删除/修改/监听(比如epoll_wait) |
| 425 | +操作。结合[协程池](#协程池),我们可以轻易地往event |
| 426 | +loop中添加非IO任务,然后在监听操作前主动调度这些任务,当然,最后触发监听操作的时间需要减去调度耗时;性能方面,直接内置thread-per-core线程池,并对任务队列前做负载均衡( |
| 427 | +由于[协程池](#协程池)和[协程窃取](#协程窃取)的存在,即使不做负载均衡也没问题)。 |
| 428 | + |
| 429 | +## Hook |
0 commit comments