协程原理讲述

文章介绍了协程的概念,它作为一种轻量级的线程,通过任务队列和挂起/唤醒机制提高并发性能。线程到协程的关键在于用户态的上下文切换,减少了系统调用的开销。文中提到了Kotlin的挂起编程、Go语言的协程调度以及腾讯的libco框架,并提供了作者自己实现的手动协程切换库作为参考。
摘要由CSDN通过智能技术生成

协程是什么

从进程和线程开始

当一个程序启动运行之后在操作系统中就成为了一个进程,操作系统会维护一个PCB数据结构作为进程的控制块,然后操作系统就会调用复杂的调度系统将进程分配给CPU核心进行执行

而由于各种并发需求的出现,人们又从进程中抽象出了一个线程的概念,线程其实就是一个执行代码逻辑流,现代CPU上其实跑的是线程执行流

线程的出现大大提高了CPU的利用率,当一个线程执行流因为IO等事件阻塞等待时,操作系统可以进行更细粒度的切换来合理利用CPU,提高了整体的性能

总体看起来就像下图(操作系统中的线程一般会通过一些技术映射控制用户线程)

image.png

当前存在的问题

线程提出其实已经足以解决很多问题,但是由于后续的各种业务的需求花样越来越多,业务的数据量也疯狂增长,在客户端和服务端的架构设计和编码上出现了一些问题:如何解决整体回调过多、如何在线程数量一定的情况下应付高并发场景

所以就在线程的基础上建立了协程的概念,这个协程从架构上看其实就是一个个任务的抽象,一个线程绑定一个任务队列,然后不断处理这些任务,一旦遇到阻塞的任务就通过一些手段将其挂起,然后切换其他任务继续运行(这里存在多种实现方式,只要不卡死在某个任务就行)

对于一些客户端的场景,个人比较了解的就是像Kotlin的挂起和唤醒编程,react后续的时间片优化等

对于服务端场景,例如go语言自带的协程调度系统、腾讯开源的libco框架等

image.png

关于协程的实现

线程运行协程和线程运行一个普通函数的根本区别就在于能不能被挂起和唤醒(类似goto)

image.png

所以要实现协程一个重要的点就是如何实现上下文切换,把线程的上下文切换放到用户态来做,就可以减少用户态转成内核态的各种开销,增大的系统的吞吐量。而要模拟切换线程上下文,其实是设置寄存器的值,还有执行的函数入口等(在linux中我们可以使用ucontext这个东西进行切换,非常方便),这里如果手动实现需要编写汇编代码,在程序运行时进行动态切换上下文

我自己实现了一个手动协程切换的库(部分还在编写中,不过切换功能已实现,供大家可以参考基本原理):
https://github.com/Prince-Hervoet/RelaxingRoutines_auto

封装一个协程结构:

class RoutineProcess
{
public:
    unsigned long long id;
    // the task func of this routine
    TaskFunc task;
    void *args;
    // the stack saved when the current coroutine was swapped out
    char *save;
    Controller *con;
    ucontext_t current;
    int status = INIT;
    int saveSize = 0;
    int capSize = 0;

public:
    RoutineProcess(unsigned long long id, TaskFunc task, void *args)
    {
        this->id = id;
        this->task = task;
        this->args = args;
    }

    ~RoutineProcess()
    {
        if (save)
        {
            delete[] save;
        }
    }
};

封装一个线程控制器:

class Controller
{
    friend void threadFunc(void *args);

private:
    // running stack: it is opened in the thread
    char runningStack[RUNNING_SIZE];

    // routines,why set?
    /*
        We're not sure if we're going to use id maps or something else,
        so we're going to use a collection, so we can just grab it.
    */
    std::set<RoutineHandler *, HandlerComparator> routineHandlers;
    unsigned long long increment;
    RoutineHandler *running;
    // epoll event list
    EpollPack *ep;
    // timer task list
    DelayQueue<RoutineEvent> *dq;
    // this context of the main routine
    ucontext_t host;
    int size = 0;
    int limit = 0;

public:
    Controller() {}
    Controller(int limit);
    ~Controller()
    {
        if (running)
        {
            delete running;
        }
        if (ep)
        {
            delete ep;
        }
        if (dq)
        {
            delete dq;
        }
        for (auto it = routineHandlers.begin(); it != routineHandlers.end(); ++it)
        {
            RoutineHandler *rh = *it;
            delete rh;
        }
        routineHandlers.clear();
    }
    RoutineHandler *createRoutine(TaskFunc task, void *args);
    void pendRoutine();
    void resumeRouine(RoutineHandler *rh);
    void removeRoutine(RoutineHandler *rh);
    void addEpollEvent(int sockfd, int eventType);
    void removeEpollEvent(int sockfd);
    void addTimedTask(int sockfd, long long will);

    RoutineHandler *getRunning()
    {
        return running;
    }
    int getSize()
    {
        return size;
    }
};

这里由于是手动库,我自己使用thread_local这个线程私有属性(JAVA的同学应该比较熟悉),只要在当前线程使用了controller,就会绑定在当前线程的这个变量上

然后这个库实现采用的是共享栈协程,就是当每个协程开始运行的时候,都是在同一个栈空间上运行,如果前一个协程没有运行完,则会被换出并保存到另一个空间中

image.png

上面的flag变量是用于后面的空间计算,因为flag变量分配的时候在最上面

其他具体的代码可以看库的实现hhh,实现起来的效果就是:

    #include "simple_routine.hpp"
    #include <iostream>

    void *test(void *args)
    {
        std::cout << "one" << std::endl;
        simple_await();
        std::cout << "three" << std::endl;
        return nullptr;
    }

    int main()
    {
        RoutineHandler *rh = simple_new(test, nullptr);
        simple_resume(rh);
        std::cout << "two" << std::endl;
        simple_resume(rh);
        return 0;
    }

    // log: one
            two
            one
            three

最后,感谢大家的阅读!欢迎有兴趣者一起讨论学习!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值