首页 > 其他分享 >跟羽夏去实现协程

跟羽夏去实现协程

时间:2024-05-04 13:33:29浏览次数:22  
标签:pushq 协程 thread 实现 羽夏去 %% popq 线程

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

引入

协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。 相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。 协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。 —— 维基百科

  我相信很多人看完这个其实是看不明白什么是协程的,提起协程,会经常拿线程作比较:

用户级线程是协作式多任务的轻量级线程,本质上描述了同协程一样的概念。其区别,如果一定要说有的话,是协程是语言层级的构造,可看作一种形式的控制流程,而线程是系统层级的构造。 —— 维基百科

  做过并发同步相关编程开发的人肯定对线程多多少少的都会清楚一些。不过继续后面的内容之前,我要增加继续引入一个概念:状态机。
  什么是状态机?维基百科解释如下:

有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automaton,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

  看起来很抽象的一个概念。其实状态机也是一个应用于万物的一个概念,打个比方:每个人都有状态,比如心情的好坏、身体健不健康、劳不劳累,这些都是状态。我们作为人每天的心情、身体状态等都随时发生着变化,不同时间都会有不同的状态,所以人是一个状态机。
  对于计算机来说,CPU是一个十分重要的器件,它拥有众多寄存器。在执行代码时,寄存器的值不断的发生变化,所以CPU也是一个状态机。对于线程的切换,其实就是CPU把寄存器的状态进行保存,把之前保存的另一个状态重新加载到寄存器中继续执行。

基础

  好了,按照我写博文的惯例,我要开始劝退啦。如果基础不够的话,要自己参考我的其他博文或者书籍补漏再回来,要不就不要继续了。

  • AT&T 格式汇编(这次我要用它写内联汇编)
  • 64 位 Intel 汇编(32 位的最起码会)
  • 会 C 语言和使用一些编译器扩展(这次用 gcc)
  • x64 平台下的调用约定

  我的相关博文:

声明

  当然,看完本篇之后或者在阅读之前你拥有基础,你可以实现自己版本的。我们这次介绍在Windows基于 Intel CPU 下的 64 位的实现。你可以实现Linux版本的,或者其他硬件平台的比如ARM平台或 32 位的硬件平台,这都你随意,因为它们的原理是相通的。
  我也有已经实现的两个版本,一个是本篇要用的代码,我放到文章后面去了。等你写完了之后再回去看作为完整参考。我还实现了一个跨WindowsLinux操作系统的 Intel CPU 下的 64 位一个协程库,功能稍微多一点,感兴趣你也可以看看,我也附到文章后面。我也会对该库提出几个问题来供你思考。
  强调一点:在你没有亲自写完一个协程之前,不要看我的代码参考,一定不要看!写完再看!
  如果你学过我的跟羽夏学 Win 内核系列的课程,里面的进程线程篇其实就已经介绍了 Intel CPU 下的 32 位的实现。不妨在看完 x64 与 x86 不同之处之后,自己来实现一份协程库之后,再来看看!

实现

  线程切换其实就是把线程的当前状态保存起来,然后加载到准备切换的线程状态。进程的状态其实就是CPU里面一堆寄存器的值,只要对这些值通过某些方式进行保存,之后加载之前保存的,不就实现线程切换了吗?
  为了尽可能的简单,这次用纯C代码来实现协程,就只实现协程创建和最简单的调度,以及程序退出之后的善后清理资源操作。
  如果你学过我的跟羽夏学 Win 内核系列的进程线程篇,你知道线程的状态是保存在栈上的。如果你没学过也没关系,知道这个结论就行。所以定义一个协程首先有个栈,还得有个栈顶指针表示使用状态。
  我们创建线程的时候必须交给一个函数指针来执行代码,当然也可以传参。传参就不在这里搞了,因为涉及调用约定的问题,就不搞稍微增加复杂度的东西。
  当然,线程也有线程的状态,比如运行中、挂起状态、死亡状态。综上所述,这个最简单的协程结构体就这么定义出来了:

typedef void (*thread_pointer)();
typedef enum thread_state { DEAD, RUNNING, SLEEP } thread_state;
typedef struct thread {
    void *stack;
    void *stackpc;
    thread_pointer pc;
    thread_state state;
} thread;

  然后我们再定义一个数组装协程(简单处理),还有一个指针指向当前运行的协程:

thread thread_table[THREAD_TABLE_MAX_SIZE];
thread *current_thread = &thread_table[0];

  如果没有时钟中断,线程切换都是主动切换的,这通常发生在调用WinAPI的时候。所以,我们需要写一个协程切换的函数:

#define THREAD_TABLE_MAX_SIZE (5)

static int swap_context_i = 0;

void swap_context_caller() {
    thread *new_thread = NULL;
    thread *old_thread = NULL;
    bool notfounded = true;
    while (true) {
        for (; swap_context_i < THREAD_TABLE_MAX_SIZE; swap_context_i++) {
            if (thread_table[swap_context_i].state == SLEEP) {
                old_thread = current_thread;
                new_thread = &thread_table[swap_context_i];
                current_thread = new_thread;
                swap_context(old_thread, new_thread);
                notfounded = false;
                break;
            }
        }
        if (notfounded) {
            swap_context_i = 0;
        } else {
            break;
        }
    }
}

  如果你有相关知识的学习很容易的发现,这其实是一个最简单的调度器。swap_context是真正实现我们协程调度的函数,但这个函数是十分特殊的,定义如下:

__attribute__((naked)) __attribute__((fastcall)) void
swap_context(thread *old, thread *new);

  __attribute__((...))是 GNU 系列编译器声明属性的一个扩展,naked是声称这个函数是裸函数,让编译器不要生成栈维护的代码。fastcall强调传参请使用这个调用约定,当然在 x64 下,这个声明是没有用的,因为默认就是这个。
  好了,不啰嗦了,给出我写的协程切换的核心函数:

#define DECLARE_STRUCT_OFFSET(type, member) [member] "i"(offsetof(type, member))

__attribute__((naked)) __attribute__((fastcall)) void
swap_context(thread *old, thread *new) {
    asm volatile("pushq %rax;"
                 "pushq %rbx;"
                 "pushq %rcx;"
                 "pushq %rdx;"
                 "pushq %rbp;"
                 "pushq %rsi;"
                 "pushq %r8;"
                 "pushq %r9;"
                 "pushq %r10;"
                 "pushq %r11;"
                 "pushq %r12;"
                 "pushq %r13;"
                 "pushq %r14;"
                 "pushq %r15;"
                 "pushfq");

    // rcx: old, rdx: new
    asm volatile("movq %%rsp,%c[stackpc](%%rcx);" ::DECLARE_STRUCT_OFFSET(
                     thread, stackpc)
                 : "rcx", "rdx");
    asm volatile("movq %0,%c[state](%%rcx);"
                 "incq %1;" ::"i"(SLEEP),
                 "m"(swap_context_i), DECLARE_STRUCT_OFFSET(thread, state)
                 : "rcx", "rdx");

    asm volatile("movq %c[pc](%%rdx),%%rax;"
                 "test %%rax,%%rax;"
                 //> if not first started
                 "jz pcnull%=;"
                 // r8 : function handler
                 "movq %%rax,%%r8;"
                 "xor %%eax,%%eax;"
                 "movq %%rax,%c[pc](%%rdx);"
                 "movq %c[stack](%%rdx),%%rbp;"
                 "movq %%rbp,%%rsp;"
                 // fill up stack info
                 "lea idle%=(%%rip), %%rax;"
                 "push %%rax;"
                 "callq *%%r8;"
                 "pcnull%=:;"
                 //> if first started
                 "movq %1,%c[state](%%rdx);"
                 "movq %c[stackpc](%%rdx),%%rsp;"
                 "jmp sw%=;"

                 "idle%=:;"
                 "call %P0;"
                 "jmp idle%=;"
                 "sw%=:;" ::"i"(swap_context),
                 "i"(RUNNING), DECLARE_STRUCT_OFFSET(thread, state),
                 DECLARE_STRUCT_OFFSET(thread, pc),
                 DECLARE_STRUCT_OFFSET(thread, stack),
                 DECLARE_STRUCT_OFFSET(thread, stackpc)
                 : "rax", "rcx", "rdx");

    asm volatile("popfq;"
                 "popq %r15;"
                 "popq %r14;"
                 "popq %r13;"
                 "popq %r12;"
                 "popq %r11;"
                 "popq %r10;"
                 "popq %r9;"
                 "popq %r8;"
                 "popq %rsi;"
                 "popq %rbp;"
                 "popq %rdx;"
                 "popq %rcx;"
                 "popq %rbx;"
                 "popq %rax;"
                 "ret;");
}

  如果你看不懂DECLARE_STRUCT_OFFSET这个东西,这个是使用了 GCC 的内联汇编扩展。这里提一嘴,微软系列的 x64 版本不能直接内联汇编,需要单独放到一个汇编代码文件中,这样十分不方便。
  如果这个协程上下文切换看不懂的话, 基本就是 AT&T 汇编不扎实或者没有画堆栈图,那就自己补补漏吧!
  创建协程的函数也就是填写一个结构体,没啥难度,先放到这里了:

bool create_thread(thread_pointer func) {
    if (func == NULL) {
        puts("create_thread failed: please input invalid excution address");
        return false;
    }

    for (int i = 0; i < THREAD_TABLE_MAX_SIZE; i++) {
        if (thread_table[i].state != DEAD) {
            continue;
        }
        thread *th = &thread_table[i];
        th->pc = func;
        th->stack = (void *)((char *)(malloc(STACK_SIZE)) + STACK_SIZE);
        th->stackpc = th->stack;
        if (th->stack == NULL) {
            puts("create_thread failed: malloc thread stack");
            return false;
        }
        th->state = SLEEP;
        return true;
    }
    return false;
}

  至此,就结束了,剩下的就靠你自己练习了。

练习与思考

  1. 我写好了,本篇文章的总示例代码在哪里?

标签:pushq,协程,thread,实现,羽夏去,%%,popq,线程
From: https://www.cnblogs.com/wingsummer/p/18170130

相关文章

  • eBPF基于LPM实现路由匹配
    基于eBPFlpmmap,icmp只有匹配上路由才能通。最终目录结构效果展示启动应用前,可以ping通192.168.0.1和192.168.0.105。启动应用后,无法ping通192.168.0.1,可以ping通192.168.0.105。停止应用后,可以ping通192.168.0.1和192.168.0.105。icmp/drop-icmp.c#include"../heade......
  • HarmonyOS 垂直方向内容滚动条实现
    概述Swiper组件是一个用户界面元素,用于在垂直方向上滚动内容。它通过遍历一个数据集合,为每一项创建一个可滚动的文本项。代码实现以下是Swiper组件的实现代码:Swiper(){ForEach(searchSwiper,(item,index)=>{Column(){Text(item).fontSize(12)......
  • c++继承两个类怎么实现
    在C++中,继承两个类可以通过多重继承来实现。多重继承允许一个派生类从多个基类继承属性和方法。以下是一个继承两个类的示例:#include<iostream>//第一个基类classBase1{public:voidmethod1(){std::cout<<"Base1method1"<<std::endl;}};//......
  • Linux 中sed命令实现从gff文件中仅仅提取基因名称
     001、(base)[b20223040323@admin1x_test]$ls##测试gff文件GCF_000001405.40_GRCh38.p14_genomic.fna.gzGCF_000001405.40_GRCh38.p14_genomic.gff(base)[b20223040323@admin1x_test]$grep-v"^#"GCF_000001405.40_GRC......
  • Linux(centos7)实现git push到gitee
    1.找到需要提交的文夹目录cd目的目录2.初始化git仓库gitinit看见最下面的InitializedemptyGitrepositoryin就是初始化成功了3.配置提交人信息gitconfig--globaluser.name“你想填的名字”gitconfig--globaluser.email"你的邮箱"最后查看一下是否正确gi......
  • C语言 Stack功能实现(自存)
    #include<stdio.h>#include<stdlib.h>#include<stdbool.h>typedefintE;typedefstructnode{Edata;structnode*next;}Node;typedefstruct{Node*top;intsize;}Stack;//APIStack*stack_create(void);void......
  • WPF上位机 - 使用转换器实现TIA Wincc中的文本列表功能
    TIAwincc中可以根据变量的值,显示出定义的文本。在WPF中可以通过转换器实现。使用哈希表存储变量和文本,根据变量值返回对应的文本显示在View中usingSystem;usingSystem.Collections.Generic;usingSystem.Globalization;usingSystem.Linq;usingSystem.Text;usingSy......
  • WPF上位机 - 使用转换器实现TIA Wincc中的位控制可见性或外观功能
    在TIAWincc中有这样的功能,使用Trueorfalse控制控件的可见性或者外观的情况。在上位机中需要使用转换器这样对Trueorfalse值转换为需要的笔刷或者Visible属性。usingSystem;usingSystem.Collections.Generic;usingSystem.Globalization;usingSystem.Linq;using......
  • 【排课小工具】排课程序设计与实现
    课表的完整性意味着,可分配的节点的数量大小等于课表周数的累加和大小,为了进行完整性检测我需要两个对象:课表模板以及课程对象,从课表模板中获取可分配的节点数,从课程对象中获取该课程的上课周次。用户要求每个班级的课表模板相同,这使得完整性检测容易很多。分级填充需求主要和课程......
  • 【排课小工具】排课程序设计与实现
    课表的完整性意味着,可分配的节点的数量大小等于课表周数的累加和大小,为了进行完整性检测我需要两个对象:课表模板以及课程对象,从课表模板中获取可分配的节点数,从课程对象中获取该课程的上课周次。用户要求每个班级的课表模板相同,这使得完整性检测容易很多。分级填充需求主要和课程......