• libco学习一


    导论

      使用 C++ 来编写高性能的网络服务器程序,从来都不是件很容易的事情。在没有应用任何网络框架,从 epoll/kqueue 直接码起的时候尤其如此。即便使用 libevent, libev 这样事件驱动的网络框架去构建你的服务,程序结构依然不会很简单。为何会这样?因为这类框架提供的都是非阻塞式的、异步的编程接口,异步的编程方式,这需要思维方式的转变,但是libevent本身是同步的,其线程是阻塞的,IO是非阻塞的。为什么 golang 近几年能够大规模流行起来呢?因为简单。这方面最突出的 一点便是它的网络编程 API,完全同步阻塞式的接口。要并发?go 出一个协程就好了。 相信对于很多人来说,最开始接触这种编程方式,是有点困惑的。程序中到处都是同步阻塞式的调用,这程序性能能好吗?答案是,好,而且非常好。那么 golang 是如何做 到的呢?秘诀就在它这个协程机制里。 在 go 语言的 API 里,你找不到像 epoll/kqueue 之类的 I/O 多路复用(I/O multiplexing) 接口,那它是怎么做到轻松支持数万乃至十多万高并发的网络 IO 的呢?在 Linux 或 其他类 Unix 系统里,支持 I/O 多路复用事件通知的系统调用(System Call)不外乎 epoll/kqueue,它难道可以离开这些系统接口另起炉灶?这个自然是不可能的。聪明的 读者,应该大致想到了这背后是怎么个原理了。

      语言内置的协程并发模式,同步阻塞式的 IO 接口,使得 golang 网络编程十分容易。那么 C++ 可不可以做到这样呢? 本文要介绍的开源协程库 libco,就是这样神奇的一个开源库,让你的高性能网络服务器编程不再困难。 Libco 是微信后台大规模使用的 C++ 协程库,在 2013 年的时候作为腾讯六大开源项目首次开源。据说 2013 年至今稳定运行在微信后台的数万台机器上。从 ArchSummit 北京峰会来自腾讯内部的分享经验来看,它在腾讯内部使用确实是比较广 泛的。同 go 语言一样,libco 也是提供了同步风格编程模式,同时还能保证系统的高发能力

    准备知识

    协程(Coroutine)是什么?

      协程这个概念,最近这几年可是相当地流行了。尤其 go 语言问世之后,内置的协程特性,完全屏蔽了操作系统线程的复杂细节;甚至使 go 开发者“只知有协程,不知有线程”了。当然 C++, Java 也不甘落后,如果你有关注过 C++ 语言的最新动态,可能也会注意到近几年不断有人在给 C++ 标准委员会提协程的支持方案;Java 也同样有一 些试验性的解决方案在提出来。

      在 go 语言大行其道的今天,没听说过协程这个词的程序员应该很少了,甚至直接接触过协程编程的(golang, lua, python 等)也不在少数。你可能以为这是个比较新的东西, 但其实协程这个概念在计算机领域已经相当地古老了。早在七十年代,Donald Knuth 在 他的神作 The Art of Computer Programming 中将 Coroutine 的提出者归于 Conway Melvin。 同时,Knuth 还提到,coroutines 不过是一种特殊的 subroutines(Subroutine 即过程调用, 在很多高级语言中也叫函数,为了方便起见,下文我们将它称为“函数”)。当调用一个函数时,程序从函数的头部开始执行,当函数退出时,这个函数的声明周期也就结 束了。一个函数在它的生命周期中,只可能返回一次。而协程则不同,协程在执行过程中,可以调用别的协程自己则中途退出执行,之后又从调用别的协程的地方恢复执行。 这有点像操作系统的线程,执行过程中可能被挂起,让位于别的线程执行,稍后又从挂起的地方恢复执行。在这个过程中,协程与协程之间实际上不是普通“调用者与被调者”的关系,他们之间的关系是对称的(symmetric)。实际上,协程不一定都是这种对称的关系,还存在着一种非对称的协程模式(asymmetric coroutines)。非对称协程其实也比较常见,本文要介绍的 libco 其实就是一种非对称协程,Boost C++ 库也提供了非对称协程

      具体来讲,非对称协程(asymmetric coroutines)是跟一个特定的调用者绑定的,协程让出 CPU 时,只能让回给原调用者。那到底是什么东西“不对称”呢?其实,非对称在于程序控制流转移到被调协程时使用的是 call/resume 操作,而当被调协程让出 CPU 时使用的却是 return/yield 操作。此外,协程间的地位也不对等,caller 与 callee 关系是确定的,不可更改的,非对称协程只能返回最初调用它的协程

      对称协程(symmetric coroutines)则不一样,启动之后就跟启动之前的协程没有任何关系了。协程的切换操作,一般而言只有一个操作yield,用于将程序控制流转移给另外的协程。对称协程机制一般需要一个调度器的支持,按一定调度算法去选择 yield 的目标协程Go 语言提供的协程,其实就是典型的对称协程。不但对称,goroutines 还可以在多个线程上迁移。这种协程跟操作系统中的线程非常相似,甚至可以叫做“用户级线程”了。而 libco 提供的协程,虽然编程接口跟 pthread 有点类似,“类 pthread 的接口设计”,“如线程库一样轻松”,本质上却是一种非对称协程。这一点不要被表象蒙蔽了。 事实上,libco 内部还为保存协程的调用链留了一个 stack 结构,而这个 stack 大小只有固定的128,128是数组大小。这个数组位于协程运行的共享环境中

    运行环境结构体

     1 struct stCoRoutineEnv_t
     2 {
     3     stCoRoutine_t *pCallStack[ 128 ];    //保存这些函数(协程看成一种特殊的函数)的调用链的栈,调用栈
     4     int iCallStackSize;
     5     /**
     6      * 这个结构也是一个全局性的资源,被同一个线程上所有协程共享。从命名也看得出来,
     7      * stCoEpoll_t 是跟 epoll 的事件循环相关的
     8      */
     9     stCoEpoll_t *pEpoll;
    10 
    11     //for copy stack log lastco and nextco
    12     stCoRoutine_t* pending_co;
    13     stCoRoutine_t* occupy_co;
    14 };

    协程调用链

    1 stCoRoutine_t *pCallStack[ 128 ];    //保存这些函数(协程看成一种特殊的函数)的调用链的栈,调用栈

      使用 libco,如果不断地在一个协程运行过程中启动另一个协程,随着嵌套深度增加就可能会造成这个栈空间溢出

    Libco 使用简介

    一个简单的例子

      在多线程编程教程中,有一个经典的例子:生产者消费者问题。事实上,生产者消 费者问题也是最适合协程的应用场景。那么我们就从这个简单的例子入手,来看一看 使用 libco 编写的生产者消费者程序(例程代码来自于 libco 源码包)。

    程序源码

     1 /*
     2 * Tencent is pleased to support the open source community by making Libco available.
     3 
     4 * Copyright (C) 2014 THL A29 Limited, a Tencent company. All rights reserved.
     5 *
     6 * Licensed under the Apache License, Version 2.0 (the "License"); 
     7 * you may not use this file except in compliance with the License. 
     8 * You may obtain a copy of the License at
     9 *
    10 *    http://www.apache.org/licenses/LICENSE-2.0
    11 *
    12 * Unless required by applicable law or agreed to in writing, 
    13 * software distributed under the License is distributed on an "AS IS" BASIS, 
    14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
    15 * See the License for the specific language governing permissions and 
    16 * limitations under the License.
    17 */
    18 
    19 #include <unistd.h>
    20 #include <stdio.h>
    21 #include <stdlib.h>
    22 #include <queue>
    23 #include "co_routine.h"
    24 using namespace std;
    25 struct stTask_t
    26 {
    27     int id;
    28 };
    29 struct stEnv_t
    30 {
    31     stCoCond_t* cond;
    32     queue<stTask_t*> task_queue;
    33 };
    34 void* Producer(void* args)
    35 {
    36     co_enable_hook_sys();
    37     stEnv_t* env=  (stEnv_t*)args;
    38     int id = 0;
    39     while (true)
    40     {
    41         stTask_t* task = (stTask_t*)calloc(1, sizeof(stTask_t));
    42         task->id = id++;
    43         env->task_queue.push(task);
    44         printf("%s:%d produce task %d
    ", __func__, __LINE__, task->id);
    45         co_cond_signal(env->cond);
    46         poll(NULL, 0, 1000);//等待1s
    47     }
    48     return NULL;
    49 }
    50 void* Consumer(void* args)
    51 {
    52     co_enable_hook_sys();
    53     stEnv_t* env = (stEnv_t*)args;
    54     while (true)
    55     {
    56         if (env->task_queue.empty())
    57         {
    58             co_cond_timedwait(env->cond, -1);
    59             continue;
    60         }
    61         stTask_t* task = env->task_queue.front();
    62         env->task_queue.pop();
    63         printf("%s:%d consume task %d
    ", __func__, __LINE__, task->id);
    64         free(task);
    65     }
    66     return NULL;
    67 }
    68 int main()
    69 {
    70     stEnv_t* env = new stEnv_t;
    71     env->cond = co_cond_alloc();
    72 
    73     stCoRoutine_t* consumer_routine;
    74     co_create(&consumer_routine, NULL, Consumer, env);
    75     co_resume(consumer_routine);
    76 
    77     stCoRoutine_t* producer_routine;
    78     co_create(&producer_routine, NULL, Producer, env);
    79     co_resume(producer_routine);
    80 
    81     co_eventloop(co_get_epoll_ct(), NULL, NULL);
    82     return 0;
    83 }

      在上面的代码中,Producer 与 Consumer 函数分别实现了生产者与消费者的逻辑, 函数的原型跟 pthread 线程函数原型也是一样的。不同的是,在函数第一行还调用了一 个 co_enable_hook_sys(),是为了开启 hook 系统调用功能,为了将整个程序的运行伪装成伪装成“同步阻塞式”的调用。此外,不是用 sleep() 去等待,而是 poll()。这些原因后文会详细解释,暂且不管,因为poll不仅需要实现等待(也就是定时事件),还要设置定时事件的回调函数以及让出CPU等。接下来我们看怎样创建和启动生产者和消费者协程。

    创建和启动生产者消费者协程

     1 int main()
     2 {
     3     stEnv_t* env = new stEnv_t;
     4     env->cond = co_cond_alloc();
     5 
     6     stCoRoutine_t* consumer_routine;
     7     co_create(&consumer_routine, NULL, Consumer, env);
     8     co_resume(consumer_routine);
     9 
    10     stCoRoutine_t* producer_routine;
    11     co_create(&producer_routine, NULL, Producer, env);
    12     co_resume(producer_routine);
    13 
    14     co_eventloop(co_get_epoll_ct(), NULL, NULL);
    15     return 0;
    16 }

      这个例子的输出结果跟多线程实现方案是相似的,Producer 与 Consumer 交替打印生产和消费信息。再来看代码,在 main() 函数中,我们看到代表一个协程的结构叫做 stCoRoutine_t, 创建一个协程使用 co_create() 函数。我们注意到,这里的 co_create() 的接口设计跟 pthread 的 pthread_create() 是非常相似的。跟 pthread 不太一样是,创建出一个协程之后, 并没有立即启动起来;这里要启动协程,还需调用 co_resume() 函数。最后,pthread 创建线程之后主线程往往会 调用pthread_join() 函数阻塞等待子线程退出,而这里的例子没有“co_join()” 或类似的函数,而是调用了一个 co_eventloop() 函数,这些差异的原因我们后文会详细解析。 然后再看 Producer 和 Consumer 的实现,细心的读者可能会发现,无论是 Producer 还是 Consumer,它们在操作共享的队列时都没有加锁,没有互斥保护。那么这样做是否安全呢?其实是安全的。在运行这个程序时,我们用 ps 命令会看到这个它实际上只有一 个线程。因此在任何时刻处理器上只会有一个协程在运行,所以不存在 race conditions, 不需要任何互斥保护

      还有一个问题。这个程序既然只有一个线程,那么 Producer 与 Consumer 这两个协 程函数是怎样做到交替执行的呢?如果你熟悉 pthread 和操作系统多线程的原理,应该 很快能发现程序里 co_cond_signal()、poll() 和 co_cond_timedwait() 这几个关键点。换作是一个 pthread 编写的生产者消费者程序,在只有单核 CPU 的机器上执行,结果是不是一样的? 总之,这个例子跟 pthread 实现的生产者消费者程序是非常相似的。通过这个例子, 我们也大致对 libco 的协程接口有了初步的了解。

    总结:

    1、libevent, libev 等事件驱动的网络框架提供的都是非阻塞式的、异步的编程接口,异步的编程方式,这需要思维方式的转变,理解和实现比较复杂。

    2、libco 通过hook机制提供了同步阻塞风格编程模式,但是这种同步时伪同步阻塞,底层还是使用的非阻塞,因为如果工作 程陷入真正的内核态阻塞,那么 libco 程序就会完全停止运转,后果是很严重的;同时还能保证系统的高并发能力。

    3、协程的分类

      非对称的协程模式

        非对称协程(asymmetric coroutines)是跟一个特定的调用者绑定的,协 程让出 CPU 时,只能让回给原调用者。那到底是什么东西“不对称”呢?其实,非对称 在于程序控制流转移到被调协程时使用的是 call/resume 操作,而当被调协程让出 CPU 时使用的却是 return/yield 操作。此外,协程间的地位也不对等,caller 与 callee 关系是 确定的,不可更改的,非对称协程只能返回最初调用它的协程。libco提供的就是非对称协程。

      对称的协程模式

        对称协程(symmetric coroutines)则不一样,启动之后就跟启动之前的协程没有任 何关系了。协程的切换操作,一般而言只有一个操作,yield,用于将程序控制流转移给 另外的协程。对称协程机制一般需要一个调度器的支持,按一定调度算法去选择 yield 的目标协程。go语言提供的是对称协程。

    4、携程当中并不需要对共享资源进行加锁来同步,因为这个程序运行的整个过程中,实际上只有一 个线程。因此在任何时刻处理器上只会有一个协程在运行,所以不存在 race conditions, 不需要任何互斥保护。

    本文来自博客园,作者:Mr-xxx,转载请注明原文链接:https://www.cnblogs.com/MrLiuZF/p/15027784.html

  • 相关阅读:
    面向对象程序设计简介(1/2)
    iOS官方Sample大全
    AFN不支持 "text/html" 的数据的问题:unacceptable content-type: text/html
    谈ObjC对象的两段构造模式
    关于self和super在oc中的疑惑与分析 (self= [super init])
    在Xcode中使用Git进行源码版本控制
    NSObject之二
    NSObject之一
    Objective-C Runtime 运行时之六:拾遗
    Objective-C Runtime 运行时之五:协议与分类
  • 原文地址:https://www.cnblogs.com/MrLiuZF/p/15027784.html
Copyright © 2020-2023  润新知