Corio - 基于 Asio 的 C++20 协程框架

最近研究了一下协程和网络编程,结合 C++20 的 Coroutine 和 Asio 编写了一个轻量级的协程框架 Corio。Corio 与 Asio 无缝集成,提供了多线程运行时支持和灵活的协程控制接口。 项目仓库:wokron/corio。 安装 corio 是一个 Header-only Library。因此只需要将本仓库 include/ 路径下的一系列头文件放在你的项目中指定位置,并将该位置添加到编译器的包含路径中即可。 以 CMake 为例,为了保持项目的模块化,可以选择将本项目作为 git 子模块。 git submodule add https://github.com/wokron/corio.git ./your/path/to/corio 随后在 CMakeLists.txt 中引入 corio。 add_library(corio INTERFACE) target_include_directories(corio INTERFACE ./your/path/to/corio/include) target_link_libraries(corio INTERFACE ${ASIO_LIBRARY}) 如前所述,Corio 依赖了 Asio。因此为了使用 Corio,还需安装 Asio(非 Boost 版本)。 最后,在你的代码中包含 corio.hpp 头文件以引入 corio。corio 的所有功能均位于 corio:: 命名空间下。 #include <corio.hpp> 使用 协程类型 lazy corio::Lazy<T> 是 corio 库的核心。通过将函数的返回值设定为 Lazy<T>,我们将该函数定义为协程。在 Lazy<T> 所定义的协程中,不再使用 return,而是使用 co_return 以便从协程中返回。其中 T 为该协程的返回值类型。...

一月 21, 2025 · 6 分钟 · 1215 字 · Wokron

编译 Tensorflow 踩坑

前段时间发现了 Tensorflow 里的一处小 Bug,现在有空正好提一个 PR。Bug 很快就修好了,不过之后进行本地编译时我却踩了不少坑。现在记录一下。 一、各种版本傻傻分不清 在开始编译之前,需要介绍一下相关的 Nvidia GPU 依赖项。 Nvidia 有不同架构的各型显卡。为了区分硬件上的区别,Nvidia 使用计算能力(Compute Capability)加以区分。计算能力版本分为两部分 x.y。大版本号表示计算架构(如 Pascal、Volta、Ampere 等等)上的变化,之间不可兼容;小版本号则表示同一架构内部的差别,更高版本可以兼容更低版本。 GPU 驱动(GPU Driver)为操作系统提供硬件驱动。其版本可以通过 nvidia-smi --query-gpu=driver_version --format=csv 找到。同一版本的驱动支持一系列不同计算能力、不同架构的显卡。 CUDA 驱动(CUDA Driver)在 GPU 驱动之上提供了 CUDA 接口。与 GPU 驱动属于内核态设备驱动不同,CUDA 驱动是一用户态的动态链接库(DSO)。CUDA 驱动的版本一般应当随着 GPU 驱动版本的更新而更新。 CUDA Toolkit 提供了构建 CUDA 程序所需的编译器、运行时和库。构建后的 CUDA 应用程序依赖于 CUDA 驱动所提供的接口。又由于 CUDA 是向后兼容(Backward Compatibility)的,所以旧的 CUDA 应用程序可以运行在新的 CUDA 驱动上;换句话说,要运行某一 CUDA 程序,需要高于特定版本的 CUDA 驱动。 向后兼容中的 Backward 指的是与时间上在前的进行兼容。这似乎是中英文导致的思维差异。 二、构建配置 在构建 Tensorflow 时添加 CUDA 支持后,Tensorflow 需要满足 CUDA Toolkit 和 CUDA 驱动之间的兼容性要求。如前所述,为了更强的兼容性,我们希望 CUDA Toolkit 的版本较低。但更高的 Tensorflow 版本又会需要更高版本的 CUDA 特性。因此在编译时需要在这两方面进行权衡。...

一月 20, 2025 · 3 分钟 · 602 字 · Wokron

讨论一下环境变量

这里所说的环境变量并不仅仅指 Shell 中的变量,而是每一个进程各自拥有的,能够通过系统 API 获取的变量。当然,Shell 通常会提供环境变量的操作方法,我们也经常通过在 Shell 中管理环境变量。但是 Shell 中的变量和环境变量实际上并不完全相同,在使用 Shell 时可能会混淆这两种概念,这里便稍微分辨一下。 一、环境变量 从程序的角度来看,环境变量很简单。环境变量是每个进程各自拥有的键值对集合。进程可以从其环境变量中读取已有变量,修改已有变量或创建新的变量。当进程创建其子进程时,子进程会继承父进程的环境变量,但子进程的环境变量的修改并不会影响父进程的环境变量。 为了方便,这里以 python 为例。 进程可以读取已有环境变量: # inherit.py import os print(os.environ["HOME"]) 也可以写入或创建环境变量: os.environ["MY_VAR"] = "my_value" 子进程会继承父进程的环境变量: # env.py import os import sys assert len(sys.argv) == 2 father_proc = bool(int(sys.argv[1])) print_my_var = lambda: print( f"process={'father' if father_proc else 'child'}, " f"MY_VAR={os.environ.get('MY_VAR', None)}" ) print_my_var() if father_proc: os.environ["MY_VAR"] = "1" print_my_var() os.system(f"{sys.executable} {sys.argv[0]} 0") print_my_var() else: os.environ["MY_VAR"] = "2" print_my_var() $ python inherit....

十一月 2, 2024 · 2 分钟 · 300 字 · Wokron

在 Conda 环境中安装 CUDA

最近想学一下 CUDA。这里记录一下配置环境的过程。 创建环境。这里只是用 Conda 实现环境隔离,不需要 python。 $ conda create -n cuda-dev $ conda activate cuda-dev 使用 nvidia-smi 查看 CUDA 版本。注意这里的版本为驱动所支持的最高 CUDA 版本。而非后面安装的 CUDA 运行时版本。在安装时应当保证 CUDA 运行时版本小于等于驱动版本。 $ nvidia-smi +---------------------------------------------------------------------------------------+ | NVIDIA-SMI 535.183.01 Driver Version: 535.183.01 CUDA Version: 12.2 | |-----------------------------------------+----------------------+----------------------+ 安装特定版本 CUDA。 $ conda install cuda -c nvidia/label/cuda-11.8.0 查看 nvcc 版本。保证驱动支持当前 CUDA 版本。 $ nvcc -V nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2022 NVIDIA Corporation Built on Wed_Sep_21_10:33:58_PDT_2022 Cuda compilation tools, release 11....

十月 14, 2024 · 1 分钟 · 88 字 · Wokron

编译技术教程:附录 - 项目测试

系列导航: 《编译技术教程:前言》 《编译技术教程:词法分析》 《编译技术教程:抽象语法树》 《编译技术教程:语法分析》 《编译技术教程:符号表与类型系统》 《编译技术教程:语义分析》 《编译技术教程:附录 - 项目测试》 项目的规模越大,越难以通过直接观察发现隐藏的代码缺陷。在实验中,各位同学需要实现的是一个万行级别的编译器项目。在这一情景下若不对自己的开发过程进行规范,及时进行项目测试,必然会严重影响代码质量,进而影响各位同学的实验进展。 因此在本附录中,我们将结合 tolangc 项目,提供一个较为简单,但具体可行的项目测试示例,以供同学们参考。 一、单元测试 (1)单元测试的结构 单元测试样例用于对程序中的最小功能模块进行测试,能够验证功能模块内部的正确性。一般来说,一个单元测试对应一个类或一个函数。对于编译器这一架构得到了充分研究的程序类型,一般来说我们可以选择为各个编译阶段设计对应的单元测试。例如在 tolangc 中,我们为 Lexer、Parser、Visitor 等等分别编写了单元测试。 一个单元测试样例是对某一功能进行的一次测试。不同的样例之间应当尽可能相互独立、互不影响。这也意味着单元测试的执行顺序不应当影响测试的结果。 需要注意的是,一些单元测试框架可能会提供设定样例执行顺序的功能,例如在某一样例成功之后执行等等。但是此处的执行顺序实际上反映的是功能模块之间的依赖关系,意为 “由于 A 模块依赖于 B 模块,所以如果 B 模块的测试出错,那么 A 模块的测试便没有意义”,而并不意味着单元测试之间可以相互关联。 由于上述特点,单元测试实际上应当看作一一个不同的可执行程序。但和整个项目所组成的程序不同,单个功能模块往往无法单独运行,且不像程序那样有着明确的输出。这就需要我们为单元测试构建测试所需的环境,并对模块的运行结果进行检验。这组成了单元测试所需要遵循的三个阶段:构造、操作和检验。 CMake 中提供的 “测试” 实际上就是这样的可执行程序。 构造指设定模块的运行环境,例如参数、依赖的对象、数据库中数据等等。构造在每个单元测试中都要进行,绝不能依赖其他单元测试的运行结果。运行环境当中需要重点关注的是可以被所有单元测试访问的单例和全局变量,以及可以实现数据持久化的文件和数据库。如果存在单元测试之间相互影响的风险,则需要在单元测试结束后进行重置操作。 操作指功能模块的运行。当构造完成运行环境之后,我们希望功能模块能够按照预期方式正确运行。 最后,检验指将功能模块的运行结果和预期结果进行比对。一般来说,测试框架会提供一系列用于比较结果的接口。具体的接口则视框架而定。这一部分的关键实际在于如何获取功能模块的运行结果。如果运行结果相对简单,如某个数值、单个数组等等,那么检验过程便相对简单。但如果运行结果的数据结构较为复杂,或运行结果位置分散,那么检验起来便较为困难。这时可以选择定义相应的信息提取函数,例如定义 to_string 函数将复杂数据结构转换为字符串。在 tolangc 中,对于语法树我们便采用了这样的处理方式。 (2)Mock 如果我们发现难以编写某个类或函数的单元测试代码,那可能意味着我们的类或函数需要进行进一步的拆分。难以测试可能是代码中的副作用或具体类之间的耦合导致的。对于类之间的耦合,我们需要遵循依赖倒置原则,用抽象的接口替代具体的实现。 例如,在实现 Lexer 时,我们没有将 ifstream 作为参数,而是将其父类 istream 作为参数。尽管对于 tolangc 来说,源代码总是从文件中读入的。 class Lexer { Lexer(std::istream &in) : _input(in) {} // ... } 这样做将使 Lexer 类更加可测试。若对 Lexer 类进行测试,我们不必创建真正的源文件。因为 istream 存在子类 istringstream。该类创建时需传入一字符串,而当从流中读取时,读到的便是传入的字符串的内容。...

九月 10, 2024 · 3 分钟 · 544 字 · Wokron

编译技术教程:语义分析

系列导航: 《编译技术教程:前言》 《编译技术教程:词法分析》 《编译技术教程:抽象语法树》 《编译技术教程:语法分析》 《编译技术教程:符号表与类型系统》 《编译技术教程:语义分析》 《编译技术教程:附录 - 项目测试》 在之前的阶段中,我们编写了词法分析和语法分析程序,实现了源代码到抽象语法树的转换,这意味着我们就此得到了结构化的语法信息;另外我们还定义了符号表,从而能够将某一处的语义存储并关联到程序的其他部分;最后我们还定义了中间代码的数据结构,让我们能够方便地构建并输出中间代码。而在这些之后,我们需要一个阶段将这些内容全部连接起来,这就是语义分析阶段。 根据语言的不同,语义分析阶段的目标也可能很不相同。例如 C 的语义分析阶段肯定与 SQL 的语义分析差别巨大。不过总体来说,语义分析阶段的思路还是类似的,那就是遍历语法树、维护符号表并构建中间代码(或者类似的翻译操作)。 一、遍历语法树 树的遍历各位同学当然不陌生。但是怎么样遍历才更好却依旧是一个值得思考的问题。假设我们现在用 Node 类表示树的节点: struct Node { int val; std::vector<Node*> children; } 那么我们很容易想到两种遍历的方式。第一种便是在 Node 中增加一个方法 visit。 struct Node { int val; std::vector<Node> children; void visit() { // do something, like `std::cout << val << std::endl;` for (auto child : children) { child.visit(); } } }; 而第二种方式则是将 Node 作为参数。为了避免使用全局变量,我们需要创建一个新的类 Visitor。 struct Visitor { void visit(Node &node) { // do something, like `std::cout << val << std::endl;` for (auto child : children) { visit(child); } } }; 那么就这两种方法来说,哪种更加适合抽象语法树的遍历呢?答案是第二种使用 Visitor 的方法。因为语义分析的关键是跨节点的语义信息传递。采取第一种方法时,信息被隔离在了每一个节点之中,想要传播只能作为 visit 函数的参数和返回值,沿着节点的遍历顺序进行传递。而对于第二种方法,类中所有方法都共享一个类作用域,这意味着信息不仅能沿着遍历顺序传递,还能跨越语法树的分支传播到需要的地方,而不需要经过层层调用。这种功能十分重要,因为实现上下文关系的关键,符号,就是跨越语法树分支存在的。...

九月 10, 2024 · 7 分钟 · 1474 字 · Wokron