分类: C++

  • [asio]学习笔记2. 概述-异步模型(Executors, Associators)

    前言

    虽然开始尝试自己基于asio写一些东西,同时也在看asio的源码,但真的想学还是应该先看作者本人写的概述:
    https://think-async.com/Asio/asio-1.30.2/doc/asio/overview.html

    这个老哥抽象能力真的很强,这些概念第一遍根本理不清楚,索性开一篇文章专门记录。

    本文记录的内容对应asio概述中这一段的内容:
    file

    其实写asio的时候会觉得很顺,但是细想才会发现这里是有大量的概念交织下才能够实现的表层写的如此顺,而且效率非常之高。

    理解asio的核心就在于理解它最核心的设计概念,但是这些概念真的很抽象,所以我在这里就是先把概念用语言梳理一遍,然后再用具体的例子把所有的概念变成代码里具体的demo,加深理解。

    异步模型

    asio的核心其实不是一个网络库,而是一个异步模型。

    asio将异步操作确立为异步组合的基本构建单元,目前支持回调函数,兼容future、纤程、协程等,也可以在上下文中混合使用这些模型和单元。

    异步操作

    如前文所述,异步操作就是具体的回调、future和协程等,具体来说用户可以在启动异步操作之后继续做其他的事情,它由一个异步操作是由启动函数和完成句柄构成的:

    file

    其中InitFunc是用户可以调用的函数,用于启动一个异步操作;Completion handler是完成句柄。为什么叫完成句柄而不是完成回调呢?

    可以从这个例子来看:

    socket.async_receive(boost::asio::buffer(buffer_), [this](const boost::system::error_code &e, std::size_t bytes) {
        if (!e) {
            // do something
        }
    });
    co_await socket.async_receive(asio::buffer(buffer), asio::use_awaitable);

    这里就启动了两个异步操作:从套接字中读取数据,两个操作分别是异步函数和C++20协程,其中async_receive就是异步操作的启动函数,它的第二个参数就是completion handler。

    对于异步函数这个异步操作而言,完成句柄就是函数本身;但是对于C++20的协程来说,完成句柄是一个asio::use_awaitable占位符,它代表两个意思:

    1. 这是一个C++20协程的占位符
    2. 会返回一个可等待对象

    这也就是完成句柄不叫完成函数的原因,除了定义完成时的行为,还需要根据句柄改变函数本身的返回值,以适应不同的异步操作。
    在这里不展开,有兴趣可以读这一篇文章,看看这种动态是如何实现的。

    异步代理

    这个概念在文档里反复出现和提及,但是因为没有很具体的实体,对我而言有点难理解,但是不理解这个概念后面围绕这个概念的设计就很难吸收。我读了三次才明白这个概念,我先按原文梳理作者的定义,后面再给出我的理解。

    file

    作者抽象了一个新的概念叫做异步代理,它是由多个异步操作通过顺序组合形成的逻辑单元,所以每个异步操作都被视为某个代理的组成部分,如下图所示:
    file

    首先按照作者的定义,异步代理有两个特点:

    1. 组成的异步操作严格串行
    2. 可以和其他的代理并行

    那例如一个socket的所有操作就不能是一个异步代理,因为读和写是可以并行的,那再深入想一下,对于一个socket的所有读操作是可以构成一个异步代理的,对于一个socket的所有写操作也是同理。

    为什么要这么区分呢?因为所有的读操作可以共用同一块内存,所有的写操作也可以公用同一块内存,这个抽象的概念是为了在开发时清晰每个buffer作用的范围,cancel实际断开的异步代理对应的异步操作链条。

    到这里就能更清晰的理解这个概念抽象的目的,异步代理不是藏在asio这个库下层的概念,而是需要深入使用者的脑子里的概念!在用的时候我们需要知道每个异步操作是在哪个代理里,同一个代理的所有操作是严格串行的,它们共用同一块资源。

    那例如我存在一个这样的需求:将某个文件按照csv格式修改一块数据并且存盘,这一整个就是一个异步代理链条,我们需要:

    1. 打开文件
    2. 按照csv格式解析
    3. 读出数据
    4. 修改数据
    5. 写入文件

    这里读写文件可以用同一片内存进行操作,数据不会存在冲突。

    但是例如前面提到的全双工的套接字,它本质上是两条异步代理,这两条异步代理的前面操作是重叠的:
    接收代理:

    1. 建立连接
    2. 接收数据
    3. 解析数据
    4. 回到2继续接受数据

    发送代理:

    1. 建立连接
    2. 等待可发送数据
    3. 发送数据
    4. 回到2继续等待

    在这个例子里,需要两套buffer,一套接收使用,一套发送使用。

    当然,如果是严格的类似于http协议的过程,请求-回复,则一条连接的收发是一个完整的异步代理。

    所以本质上作者在这里提到异步代理是希望开发者在开发时,深入考察是否哪些异步操作可以并行,哪些异步操作之间存在临界区。在抽象出来这个概念之后,一个异步代理的所有操作就像是一个线程里的同步操作。

    异步代理的特指和关联器

    异步代理关联的异步操作具有一些关联性的特征,是需要开发者开发或者处理的,例如:

    1. 分配器,决定异步操作如何获得内存资源
    2. 取消槽,如何支持取消异步操作
    3. 执行器,决定了异步完成代理是如何调度和执行的

    子代理

    file

    如上图所示,这个更像是例如多个异步操作之间存在层的概念的一种封装,类似于HTTP层服务端等待数据抵达,会触发ssl层的数据收发,ssl层又等待tcp层,ssl层在tcp层数据抵达之后可能要等待多次ssl层的握手,才告诉http层,收到数据了。

    这里子代理的概念是类似于这样的实例的抽象,可以将一整个代理封装成一个简单的异步事件。

    执行器

    执行器就是在完成之后决定如何执行完成句柄,对应到代码里就是executor的概念,对应到库里面其实就是io_context, strand这些,在构造socket对象的时候会传入的那个参数。

    内存分配器

    这里的内存分配器是特指库里的一个接口,每个异步操作都会关联一个分配器,用来给异步操作获取每次可稳定操作的内存资源(POSMs)。这个名字本身也反映了两个特质,内存是按每次操作进行分配的(因为内存在该操作的生命周期内保留),并且是稳定的。

    异步操作可以有多种方式来分配POMs,用户可以忽略分配器,使用默认的分配器,也可以根据需要去定制分配器。

    取消

    定时器和套接字都支持close或者cancel取消操作,同时某些异步操作还支持对单次操作。
    为了支持取消操作,需要向代理对象的插槽中注册一个取消处理函数。取消处理函数是当用户发出取消信号时会被触发执行,由于取消插槽和单个代理体绑定,所以同时只支持一个处理程序。
    可以注册覆盖新的函数去覆盖旧函数。

    End.

  • ImGui天下第一

    之前听油管的up提过imgui,然后今天看到几个地方都说这玩意儿好用,在维护的时候就拉下来跑了一下。
    (跑完)
    ImGui天下第一!
    开发太方便了!不需要设置任何回调,按钮按下在下次tick就会返回True

                static float f = 0.0f;
                static int counter = 0;
    
                ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.
    
                ImGui::Text("This is some useful text.");               // Display some text (you can use a format strings too)
                ImGui::Checkbox("Demo Window", &show_demo_window);      // Edit bools storing our window open/close state
                ImGui::Checkbox("Another Window", &show_another_window);
    
                ImGui::SliderFloat("float", &f, 0.0f, 1.0f);            // Edit 1 float using a slider from 0.0f to 1.0f
                ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color
    
                if (ImGui::Button("Button"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
                    counter++;
                ImGui::SameLine();
                ImGui::Text("counter = %d", counter);
    
                ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
                ImGui::End();

    只要让这段代码一直循环,当按钮按下的时候counter就会自动++
    是从来没有设想过的开发方式…!

    它利用了一个很基本的原理,延迟一帧处理用户感知不到,然后将所有的输入缓存然后直接存到对应的输入组件里。。

    这个设计思想太牛了!!

  • std::map学习笔记

    本来只是想草草读一下这篇文章:https://142857.red/book/stl_map/
    但是还是发现一些值得记录的点,在这里记录一下:
    []读取不存在的key会报错

    map<string, int> config = {
        {"timeout", 985},
        {"delay", 211},
    };
    print(config["timeout"]); // 985
    print(config["tmeout"]);  // 默默返回 0

    推荐所有的读取都用at:

    map<string, int> config = {
        {"timeout", 985},
        {"delay", 211},
    };
    print(config.at("timeout"));  // 985
    print(config.at("tmeout"));   // 该键不存在!响亮地出错

    []会自动创建不存在键值

    map<string, int> config = {
        {"delay", 211},
    };
    config.at("timeout") = 985;  // 键值不存在,报错!
    config["timeout"] = 985;     // 成功创建并写入 985

    因此它引入了一条原则:

    • 读取元素时,统一用 at()
    • 写入元素时,统一用 []

    C++不是python,auto要显示指定引用

    void PeiXunCpp(string stuName) {
        auto stu = stus.at(stuName);  // 这是在栈上拷贝了一份完整的 Student 对象
        stu.money -= 2650;
        stu.skills.insert("C++");
    }

    在python类似的写法是没有问题的,但是在C++中这个stu左边的auto没有显示指定&为引用,则在实际构造的过程中会发生一次拷贝构造函数,并且后续的修改不会贴回到stus的字典里。

    End.