图集1/8

正文 3869字数 186,984阅读

一、背景

为什么需要网络协程?

1、协程/纤程并不是一个新概念

2、大并发、高性能对于服务端的高要求

3、移动设备的快速增长加大了服务端大并发压力

4、Go 语言的兴起将协程带到了一个新的高度

支持协程的编程语言:

1、Go 语言,非常容易支持大并发、高性能

2、Python 语言

3、Erlang 语言

4、Lua 语言
。。。。。。

为什么要设计一套 C/C++ 网络协程库?

1、学习一部门语言的成本要远高于学习一个库

2、C/C++ 程序员多年的经验积累损耗巨大

3、C/C++ 综合运行效率高

二、关于并发

– 虽已进入多核时代,但服务器的 CPU 核心总是有限的

– 当进程/线程数越多操作系统的调度算法就越低效

– TCP长连接及连接池的存在,造成服务端80%以上的连接是空闲的

为支持并发,我们需要采用:

1、多进程模式:支持并发能力非常有限,如 Postfix,Xinetd;

2、多线程模式:比多进程模式有提高,但依然有限,如 Mysql;

3、非阻塞模式:性能高,但编程复杂度极高,如 Nginx,Redis;

4、基于事件的多线程模式:并发度有较大提高,但编程提升依然有限,如 acl 中的 master_threads 服务模式;

三、设计目标

我们需要一种新的编程模式来满足C/C++程序员:

1、支持大并发、高性能,较低的资源使用率

2、较低的编程复杂度:顺序思维模式

3、适合多数应用场景,提供丰富且简单易用的接口

4、与第三方网络库无缝集成,无需修改第三方库

四、一个简单的协程示例

1、创建协程类似于创建线程
2、支持大并发、高性能
3、顺序性编程方式
4、无需更改第三方库
5、仅使用一个线程资源
五、协程的调度方式

1、上下文切换
通过操作系统提供的 API 完成:getcontext、makecontext、swapcontext、setcontext;
或 自己通过汇编语言来实现协程运行栈空间的切换
实现库举例:libtask,boost,libgo, libco,coroutine 等

2、信号跳转

通过系统提供的 API 完成:siglongjmp、longjmp、setjmp、sigsetjmp 等
实现库举例:libmill,st ,coroutine 等
六、协程切换方式

七、网络协程调度

1、IO事件协程监控所有的IO事件
2、网络协程运行时遇到IO阻塞,则被挂起,其IO句柄由IO事件协程监控
3、IO事件发生时,其绑定的协程被再次唤醒

八、如何与第三方库无缝集成

1、HOOK IO相关API
读 API:read/readv/recv/recvfrom/recvmsg 写 API:write/writev/send/sendto/sendmsg 其它 API:pipe/popen/pclose/open/close/fcntl
Run code
Cut to clipboard


    2、HOOK 网络相关API
    socket/socketpair/bind/listen/accept/connect poll/select/epoll_create/epoll_wait/epoll_ctl gethostbyname/gethostbyname_r
    Run code
    Cut to clipboard


      通过 HOOK 系统底层 API,可以实现:

      1、直接接管第三方库(如:mysql/http/redis 等库)的网络连接及通信过程
      2、直接接管第三方库的域名解析过程
      3、将第三方网络阻塞过程协程化,在协程库底层转化为非阻塞过程

      将mysql库协程化的例子参见:acl/lib_fiber/samples/mysql

      九、为何要 HOOK 很多系统API

      1、poll/select 为网络编程中常用系统 API
      2、很多第三方网络库用 poll/select 模拟IO超时
      3、epoll 在 reactor 类应用(如:聊天)方面比较广泛
      4、gethostbyname 在域名解析方面应用广泛
      5、listen 需要将监听描述字设为非阻塞模式
      6、connect 需要将连接描述字设为非阻塞模式
      7、bind/socket/socketpair/。。。为便于将出错号与协程绑定

      十、基于协程的 errno

      因为每个线程中存在大量协程,当某个协程的IO过程出错时,如果实现不同协程之间的 errno 是相互隔离的?

      — 在 Linux 平台下直接 HOOK __errno_location 系统函数
      参见:/usr/include/bits/errno.h extern int *__errno_location (void) __THROW __attribute__ ((__const__)); #define errno (*__errno_location ())
      Run code
      Cut to clipboard


        针对进程内全局变量:errno,操作系统将该变量定义为一个函数指针地址,函数内部会通过线程局部变量方式给每一个线程分配一个 error 对象

        因此,通过 hook __errno_location 函数,在协程库里给每个协程一个协程局部变量,实现了 errno 全局变量的协程安全性

        十一、内存安全检测

        配合 valgrind 做内存检测:

        – valgrind 与 xxxcontext 的不兼容性
        – 需下载 valgrind 开发包,调用 VALGRIND_STACK_REGISTER通知

        valgrind 跳过检测该内存区域

        – 检测时在 Makefile 里打开 –DUSE_VALGRIND 编译选项,重新编译 lib_fiber.a

        十二、有效使用多核

        每个线程一个独立的协程调度器,通过创建多个线程使用多核

        使用 acl master 服务器框架,创建多进程使用多核,每个进程一个协程调度器

        多线程示例参见:acl/lib_fiber/samples/redis_threads

        多进程示例参见:acl/lib_fiber/samples/master_fiber

        十三、协程同步原语

        基于协程的协程锁:

        1、协程互斥锁
        2、协程读写锁

        十四、协程挂起与唤醒

        — 协程挂起方式

        1、主动让出 CPU 控制权

        当前运行的协程通过调用 acl_fiber_yield 主动让出 CPU 控制权,协程调度器调用别的协程

        2、指定休眠时间

        当前运行的协程通过调用 acl_fiber_sleep 使当前协程休眠指定时间

        3、IO阻塞被挂起

        当前运行的协程等待IO完成时,需要将自身挂起

        — 协程唤醒方式

        1、主动 yield 的协程又重新获得 CPU 控制权
        2、处于休眠状态的协程时间到达
        3、因IO阻塞而被挂起的协程因IO准备好而被唤醒

        示例参考:
        1、yield 方式:acl/lib_fiber/samples/fiber 2、sleep 方式:acl/lib_fiber/samples/sleep 3、IO 方式:acl/lib_fiber/samples/select
        Run code
        Cut to clipboard


          十五、过载保护

          十六、协程间通信

          协程间为什么需要通信?

          1、业务逻辑的模块化
          2、业务模块的分层设计
          3、团队开发的协作性

          协程间“通信”的本质:

          – 协程间数据的传递通过协程上下文的切换,本质上是协程间的数据交换

          协程间“通信”的成本:

          1、协程上下文切换
          2、内存分配、释放
          3、数据拷贝

          协程间“通信”方式:

          – 支持多对多数据交互

          – 协程通信管道支持多对多方式
          – 协程间通信通过切换协程上下文及数据交换完成
          – 协程间通信时的数据交换支持缓冲模式
          – 协程间通信时的数据交换采用随机分配方式

          十七、线程间通信

          协程模式下为何需要线程间通信?

          – 为使用多核,开启多个线程,线程间需要交换数据
          – 有些任务需要在线程池里异步完成,结果需要传递给主线程

          协程模式下线程间的通信方式:

          – 无锁消息队列 + IO 模式

          十八、线程间通信

          1、生产者/消费者之间优先通过无锁队列进行数据传递
          2、当生产者无数据时,消费者通过IO堵塞
          3、当消费者堵塞在IO等待新消息时,生产者若有新消息则通过IO通知消费者
          4、无锁队列利用率越高,则处理性能越高

          十九、应用场景

          (一)、问答式应用服务

          基于 HTTP 协议的服务应用,诸如:网站

          基于 SMTP/POP3/IMAP 协议的服务应用

          (二)、生产者 – 消费者类应用服务

          如消息队列类应用

          (三)、reactor 和 proactor 两种模式的结合

          统一的事件引擎监控所有的网络连接,有一个连接就绪时创建协程独立处理

          此类应用如聊天服务、游戏服务等无状态的应用服务

          (四)、大并发类应用服务

          因为通过协程方式,将上层应用的堵塞式在底层转为非阻塞模式,所以非常容易以较低资源支持大并发类应用

          如内网的多数应用服务为提高效率都支持连接池模式,需要服务端支持非常大的并发

          (五)、网络限流

          在协程中可以直接 sleep,非常容易控制网络流量

          二十、协程编程注意事项

          (一)、协程运行堆栈空间的合理分配

          每个协程都需要分配一定的内存空间用于上下文的切换,如果分配大了则会造成内存浪费,分配小了可能造成意外不可恢复的崩溃

          一般情况下,每个协程分配32KB ~ 320KB

          (二)、协程间需要协作,防止有的忙死,有的饿死

          当协程长期占用 CPU 时,应该主动 yield 让出 CPU

          (三)、协程内防止有堵塞式操作,以防堵塞当前线程中的所有协程

          应通过对业务逻辑模块进行分类,确定不同的协程工作方式,使堵塞操作放在线程池中运行