• 基于纤程(Fiber)实现C++异步编程库(一):原理及示例


    纤程(Fiber)和协程(coroutine)是差不多的概念,也叫做用户级线程或者轻线程之类的。Windows系统提供了一组API用户创建和使用纤程,本文中的库就是基于这组API实现的,所以无法跨平台使用,非Windows程序员可以闪人了,当然如果有兴趣可以继续看下去,找个第三方的协程库封装一下,也能实现相同的效果。关于纤程更详细的信息可以查阅MSDN

    纤程的概念中有两个关键点:

    1. 纤程拥有独立的栈空间和寄存器环境
    2. 纤程在用户态实现调调度,也就是说完全由程序员控制;

    下图演示了几个纤程相互切换的过程,注意每个纤程都有独立的栈,并且通过SwitchToFiber函数切换到其他纤程:

    作为对比,我们可以看一下函数调用过程中的堆栈变化情况,下面是示意图,表示了func1 -> func2 -> func3 这种常见的函数嵌套调用关系:

     

    每一次函数调用都会创建一个新的栈帧(stack frame),合起来就构成整个调用栈,函数返回时其栈帧也随之释放。对于函数调用,我们可以确定的一点是(在不抛出异常的情况下)被调用函数执行完毕后一定会在调用点返回并继续执行下一条语句。但纤程之间的调用(切换)却不同,一个纤程可以在任意位置切换到其他纤程,并且可能永远都不会再切换回来,也可能从其他任意纤程(不必是刚刚切换到的)切换回来,前面的示意图描述的只是一种非常简单的情况,实际的情况可能非常复杂,复杂到导出都是跳来跳去的箭头理也理不清。在纤程间切换,有点像用加强版的goto,用的时候固然很爽,但后续的维护却是个麻烦。

    所以就像用while/for/switch-case代替goto一样,我们也需要封装一组新的API来代替对操作系统API的直接调用。一方面,在封装过程中我们可以对纤程的行为(实际是程序员的行为)施加一些安全约束,使得更容易写出安全的代码或者更不容易写出不安全的代码;另一方面,从goto到while/switch等过程控制语句实际上是一种抽象层次的提升,对大部分常见需求后者用起来更方便,更不容易出错,写出的代码也更简洁易懂,类似的,从系统API到新的封装API或者封装类也是抽象层次的提高,可以更方便的应用在各种业务场景;最后,直接使用系统API需要写很多维护纤程的辅助代码,这类代码通常重复而又分散到业务代码的各个角落,进一步降低了程序的可读性和提高了维护难度,封装也是为了解决这个问题。

    好了,废话说完了,我们先上一段代码尝尝鲜:

     1     const int RUN_TIMES = 5;
     2 
     3     int number = 0;
     4     bool shutdown = false;
     5 
     6     Fiber fib([&number, &shutdown]
     7     {
     8         while (!shutdown)
     9         {
    10             number++;
    11             Fiber::yield();             // A:控制权移交到主纤程
    12         }
    13 }); 14 15 for (int i = 0; i < RUN_TIMES; i++) 16 { 17 fib.resume(); // B: 切换到子纤程执行 18 } 19 20 printf("number = %d ", number);

    这里先创建了一个纤程实现number变量累加的功能,然后在for循环中执行(姑且用这个词)最终得到正确的结果。AB两处代码分别实现了纤程的切换,实际上是封装了对SwitchToFiber的调用,注意两个函数调用细节上的不同:resume表示切换到对象包装的纤程,是普通成员函数,yield表示控制权移交给调用者纤程,是静态成员函数,大家可以思考下为什么有静态和非静态成员函数的差别。

    下面是用纤程实现生产者-消费者模型的代码:

     1     int product_count = 0;
     2     bool is_end_time = false;
     3 
     4     const int RUN_TIMES = 3;
     5 
     6     // 生产者纤程
     7     Fiber fib_producer([&is_end_time, &product_count]
     8     {
     9         srand((unsigned)time(NULL));
    10 
    11         while (!is_end_time)
    12         {
    13             int new_product_count = (int)((double)rand() / RAND_MAX * 10) + 1;
    14             product_count += new_product_count;
    15 
    16             printf("[producer] create new products: %d
    ", new_product_count);
    17 
    18             Fiber::yield();
    19         }
    20 
    21         printf("[producer] off duty.
    ");
    22     });
    23 
    24     // 消费者纤程的执行函数
    25     auto consumer_proc = [&is_end_time, &product_count](const int seq_number)
    26     {
    27         int total_count = 0;
    28 
    29         while (!is_end_time)
    30         {
    31             if (product_count > 0)
    32             {
    33                 product_count--;
    34                 total_count++;
    35                 printf("[consumer %d] got 1 product, total got %d, remain %d
    ", seq_number, total_count, product_count);
    36             }
    37 
    38             Fiber::yield();
    39         }
    40 
    41         printf("[consumer %d] off duty.
    ", seq_number);
    42     };
    43 
    44     const int CONSUMER_COUNT = 3;
    45     int consumer_seq_number = 0;
    46 
    47     // 创建消费者纤程数组
    48     std::vector<Fiber> consumer_array(CONSUMER_COUNT);
    49     std::for_each(consumer_array.begin(), consumer_array.end(), [&](Fiber& item){ item = Fiber([&]{ consumer_proc(consumer_seq_number); }); consumer_seq_number++; });
    50 
    51     consumer_seq_number = 0;
    52 
    53     for (int i = 0; i < RUN_TIMES; i++)
    54     {
    55         fib_producer.resume();
    56 
    57         while (product_count > 0)
    58         {
    59             consumer_array[consumer_seq_number].resume();
    60             consumer_seq_number = (consumer_seq_number + 1) % CONSUMER_COUNT;
    61         }
    62     }
    63 
    64     is_end_time = true;
    65 
    66     // 等待纤程结束
    67     Fiber::await_all(consumer_array);
    68     Fiber::await(fib_producer);

    程序末尾出现了await和await_all两个新的方法可以先不用管,不影响主要逻辑。由于所有纤程都是在同一个线程中运行的所以无需加锁,这也是使用纤程的一个重要好处,也是我们这个封装库的主要目的之一。

    限于篇幅,这次就只写这么多了,更多的内容将放到后面的帖子中,总计还要写四、五篇的样子。但代码实际上已经写完了,急性子的园友可以直接到这个地址看代码:

    https://code.csdn.net/xrunning/fiber

    建了一个QQ群:微观架构设计165241092,主要讨论C++代码级设计,感兴趣的园友加进来一起讨论学习。 

    作者:小时了了
    原创文章,欢迎转载,但请以链接形式注明本文地址.
  • 相关阅读:
    laravel 使用构造器进行增删改查
    explan各项说明
    data函数参数
    php引用
    PHP开发api接口安全验证方法一
    redis主从配置
    php 实现同一个账号同时只能一个人登录
    MySQL慢查询1- 开启慢查询
    前端基础 & 初识HTML
    HTTP协议概述
  • 原文地址:https://www.cnblogs.com/xrunning/p/4176331.html
Copyright © 2020-2023  润新知