https://think-async.com/Asio/asio-1.30.2/doc/asio/overview/core/reactor.html
竟然直接支持reactor的形式,虽然支持好像不太难,但这个小细节确实挺牛的。
https://think-async.com/Asio/asio-1.30.2/doc/asio/overview/core/reactor.html
竟然直接支持reactor的形式,虽然支持好像不太难,但这个小细节确实挺牛的。
虽然开始尝试自己基于asio写一些东西,同时也在看asio的源码,但真的想学还是应该先看作者本人写的概述:
https://think-async.com/Asio/asio-1.30.2/doc/asio/overview.html
这个老哥抽象能力真的很强,这些概念第一遍根本理不清楚,索性开一篇文章专门记录。
本文记录的内容对应asio概述中这一段的内容:

其实写asio的时候会觉得很顺,但是细想才会发现这里是有大量的概念交织下才能够实现的表层写的如此顺,而且效率非常之高。
理解asio的核心就在于理解它最核心的设计概念,但是这些概念真的很抽象,所以我在这里就是先把概念用语言梳理一遍,然后再用具体的例子把所有的概念变成代码里具体的demo,加深理解。
asio的核心其实不是一个网络库,而是一个异步模型。
asio将异步操作确立为异步组合的基本构建单元,目前支持回调函数,兼容future、纤程、协程等,也可以在上下文中混合使用这些模型和单元。
如前文所述,异步操作就是具体的回调、future和协程等,具体来说用户可以在启动异步操作之后继续做其他的事情,它由一个异步操作是由启动函数和完成句柄构成的:

其中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占位符,它代表两个意思:
这也就是完成句柄不叫完成函数的原因,除了定义完成时的行为,还需要根据句柄改变函数本身的返回值,以适应不同的异步操作。
在这里不展开,有兴趣可以读这一篇文章,看看这种动态是如何实现的。
这个概念在文档里反复出现和提及,但是因为没有很具体的实体,对我而言有点难理解,但是不理解这个概念后面围绕这个概念的设计就很难吸收。我读了三次才明白这个概念,我先按原文梳理作者的定义,后面再给出我的理解。

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

首先按照作者的定义,异步代理有两个特点:
那例如一个socket的所有操作就不能是一个异步代理,因为读和写是可以并行的,那再深入想一下,对于一个socket的所有读操作是可以构成一个异步代理的,对于一个socket的所有写操作也是同理。
为什么要这么区分呢?因为所有的读操作可以共用同一块内存,所有的写操作也可以公用同一块内存,这个抽象的概念是为了在开发时清晰每个buffer作用的范围,cancel实际断开的异步代理对应的异步操作链条。
到这里就能更清晰的理解这个概念抽象的目的,异步代理不是藏在asio这个库下层的概念,而是需要深入使用者的脑子里的概念!在用的时候我们需要知道每个异步操作是在哪个代理里,同一个代理的所有操作是严格串行的,它们共用同一块资源。
那例如我存在一个这样的需求:将某个文件按照csv格式修改一块数据并且存盘,这一整个就是一个异步代理链条,我们需要:
这里读写文件可以用同一片内存进行操作,数据不会存在冲突。
但是例如前面提到的全双工的套接字,它本质上是两条异步代理,这两条异步代理的前面操作是重叠的:
接收代理:
发送代理:
在这个例子里,需要两套buffer,一套接收使用,一套发送使用。
当然,如果是严格的类似于http协议的过程,请求-回复,则一条连接的收发是一个完整的异步代理。
所以本质上作者在这里提到异步代理是希望开发者在开发时,深入考察是否哪些异步操作可以并行,哪些异步操作之间存在临界区。在抽象出来这个概念之后,一个异步代理的所有操作就像是一个线程里的同步操作。
异步代理关联的异步操作具有一些关联性的特征,是需要开发者开发或者处理的,例如:

如上图所示,这个更像是例如多个异步操作之间存在层的概念的一种封装,类似于HTTP层服务端等待数据抵达,会触发ssl层的数据收发,ssl层又等待tcp层,ssl层在tcp层数据抵达之后可能要等待多次ssl层的握手,才告诉http层,收到数据了。
这里子代理的概念是类似于这样的实例的抽象,可以将一整个代理封装成一个简单的异步事件。
执行器就是在完成之后决定如何执行完成句柄,对应到代码里就是executor的概念,对应到库里面其实就是io_context, strand这些,在构造socket对象的时候会传入的那个参数。
这里的内存分配器是特指库里的一个接口,每个异步操作都会关联一个分配器,用来给异步操作获取每次可稳定操作的内存资源(POSMs)。这个名字本身也反映了两个特质,内存是按每次操作进行分配的(因为内存在该操作的生命周期内保留),并且是稳定的。
异步操作可以有多种方式来分配POMs,用户可以忽略分配器,使用默认的分配器,也可以根据需要去定制分配器。
定时器和套接字都支持close或者cancel取消操作,同时某些异步操作还支持对单次操作。
为了支持取消操作,需要向代理对象的插槽中注册一个取消处理函数。取消处理函数是当用户发出取消信号时会被触发执行,由于取消插槽和单个代理体绑定,所以同时只支持一个处理程序。
可以注册覆盖新的函数去覆盖旧函数。
End.
之前听油管的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就会自动++
是从来没有设想过的开发方式…!
它利用了一个很基本的原理,延迟一帧处理用户感知不到,然后将所有的输入缓存然后直接存到对应的输入组件里。。
这个设计思想太牛了!!
本来只是想草草读一下这篇文章: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
因此它引入了一条原则:
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.
因为开发机的gcc版本太低了,就重新编译了一个clang,大概了解了一下这些底层工具的概念。
clang++和clang都是llvm的一部分,而llvm是一个庞大的、模块化编译器的项目,clang++只是基于llvm的编译器前端。
所以对于一个C++编译过程,从.cpp生成.o,在llvm的这个项目下的流程是:
所以我们想编译一个完整的clang++,是在llvm项目,因为clang++依赖llvm作为后端,而clang++只是llvm的一种前端。
这个是clang实现的一版C++标准,不同于gnu gcc的实现。
再编译了之后,我发现还是无法编译C++20的内容,提示找不到<coroutine>,那很自然是库的问题,我们可以在llvm的官网看到项目包含的子项目:
https://llvm.org/
除了clang,还有libc++, libc++ABI和libc。
我重新编译了之后还是有问题,最终是在加了参数-stdlib=libc++之后才能正常编译,并且编译成功之后还是没办法正常的运行,我手动把/usr/local/lib/x86_64-unknown-linux-gnu/目录下的libc++.so.1用软连接放到/usr/lib下才全部搞定。
这里我才知道,原来编译器和标准库是拆开的,编译器是clang,标准库可以用gnu gcc的,也可以用clang自己的libc++,这里列一下C++的标准实现:
https://developer.huawei.com/consumer/cn/forum/topic/41600287
https://libcxx.llvm.org/
https://llvm.org/
https://llvm.org/docs/CMake.html
End。
打算基于asio写多种序列化库的测评,在底层用同一个asio构造函数的方式,然后上层测试脚本里切换序列化的实现。
但是最开始按着demo写逻辑就出现了问题,我想先纯面向过程,就没像demo里写一个connection类,然后就探究到一直会闪退的问题。
最后定位到时ip::tcp::socket析构的时候会断开连接。
socket的大概是这样的结构:
typedef basic_stream_socket<tcp> socket;
template <typename Protocol, typename Executor>
class basic_stream_socket
: public basic_socket<Protocol, Executor>;
{};
template <typename Protocol, typename Executor>
class basic_socket: public socket_base
{
// ....
~basic_socket()
{
}
#if defined(BOOST_ASIO_WINDOWS_RUNTIME)
detail::io_object_impl<
detail::null_socket_service<Protocol>, Executor> impl_;
#elif defined(BOOST_ASIO_HAS_IOCP)
detail::io_object_impl<
detail::win_iocp_socket_service<Protocol>, Executor> impl_;
#elif defined(BOOST_ASIO_HAS_IO_URING_AS_DEFAULT)
detail::io_object_impl<
detail::io_uring_socket_service<Protocol>, Executor> impl_;
#else
detail::io_object_impl<
detail::reactive_socket_service<Protocol>, Executor> impl_;
#endif
};
大概是这样的关系,虽然basic_socket, basic_stream_socket和tcp都没有在析构函数里做逻辑。但是实际实现操作系统连接句柄的impl_的析构函数里有做逻辑的,而且实现的方式还挺巧妙。
Windows的io_uring_socket_service和Linux的reactive_socket_service都没有在析构里做逻辑,这一部分是在detail::io_object_impl里做的。
template <typename IoObjectService,
typename Executor = io_context::executor_type>
class io_object_impl
{
public:
typedef IoObjectService service_type;
// Construct an I/O object using an executor.
explicit io_object_impl(int, const executor_type& ex)
: service_(&boost::asio::use_service<IoObjectService>(
io_object_impl::get_context(ex))),
executor_(ex)
{
service_->construct(implementation_);
}
// Destructor.
~io_object_impl()
{
service_->destroy(implementation_);
}
private:
// The service associated with the I/O object.
service_type* service_;
};
也就是io_object_impl本身实现的是IoObjectService的生命周期管理,对操作系统的io对象进行统一的封装,给上层提供统一的接口。然后通过模板类可以切换实际实现的方式。
同时如小标题所示,ip::tcp::socket这类对象在析构的时候会断开连接,所以在asio实际使用过程中,一定要抓住socket的生命周期。
顺便深入看一下tcp的连接实现细节,我们以Linux的视角看一下实现细节。
我们从最底层看起,basic_socket继承自socket_base,socket_base是一个定义了全双工半双工状态,当前等待状态的抽象类。它的抽象体现在析构函数实现在protetecd里,只有子类对象可以被析构。
class socket_base
{
public:
/// Different ways a socket may be shutdown.
enum shutdown_type
{
#if defined(GENERATING_DOCUMENTATION)
/// Shutdown the receive side of the socket.
shutdown_receive = implementation_defined,
/// Shutdown the send side of the socket.
shutdown_send = implementation_defined,
/// Shutdown both send and receive on the socket.
shutdown_both = implementation_defined
#else
shutdown_receive = BOOST_ASIO_OS_DEF(SHUT_RD),
shutdown_send = BOOST_ASIO_OS_DEF(SHUT_WR),
shutdown_both = BOOST_ASIO_OS_DEF(SHUT_RDWR)
#endif
};
// ....
protected:
/// Protected destructor to prevent deletion through this type.
~socket_base()
{
}
};
template <typename Protocol, typename Executor>
class basic_socket
: public socket_base
{
detail::io_object_impl<
detail::reactive_socket_service<Protocol>, Executor> impl_;
};
这个对象是对套接字做的一个高级抽象,类似于Linux里万物都是socket,可以read and write,具体的上层协议本身是定义在Protocol里面,Protocol包含协议本身以及endpoint,也就是地址。
例如TCP的endpoint就是IP和地址,感觉如果未来KCP或者其他协议,可以直接定义一个新的endpoint类,增加多个channel就能实现很多东西,没必要用多个实际上的操作系统套接口?
这些实际操作系统的实现又落地在impl_里,impl_实际上是一个io_object,不同的系统会是不同的实现,但是保持统一的对外接口,大概如下图:

我们从最底下向上看,在每个操作系统底层其实有两个部分,service_和implement_,他们互为一组向上和向下的关系抽象,implement_是service_类里的一个struct,主要包含该操作系统API下的资源细节,通常包含套接口句柄,上层协议类型,以及其他的一些数据。
例如在Linux下,所有的socket都是用int类型的一个句柄,无论是Tcp还是文件还是Udp协议都是同一个句柄。所以implement_里存了套接口以及具体的Tcp还是Udp协议。
service_则是对操作系统的API提供的一层service抽象,将不同的操作系统的IO接口提供成统一的API,这里实现了创建、连接、收发消息和关闭连接。
而整个service_会被basic_socket用作操作系统无关的io对象的实现,而基于这些,basic_socket向上层提供了连接和异步的基本能力,创建、管理、异步等待等…
再进一步basic_stream_socket则是向上层提供了进一步的读写能力。
本次就先读到这里吧。
End.
C++20拥有一个全新的特性:协程。
我来从python的角度来解释C++这个特性设计与其他语言的不同,目的以及意义。
协程是一种可以挂起和恢复执行的函数。C++20协程跟python的生成器是很相似的,如果函数中出现了co_yield, co_return, co_await,那么这个函数就是协程函数。而协程的本质就是将一个函数拆分成多个不能控制执行顺序,但是可以控制执行时机的语言结构。
对普通函数而言,执行一个函数是开始运行你的逻辑;但是对于协程来说,执行协程函数会先创建一个协程对象,但是是否开始执行还是挂起是视情况而定的。
例如这个python代码:
def hello():
print("Start run")
ret = yield2
print("After suspend", ret)
yield3
print("After Suspend")
def main():
generator=hello()
print(generator)
ret1 = next(generator)
print("ret1: ", ret1)
ret2 = generator.send(23)
print("ret2: ", ret2)
generator.send(23)
在这里hello就是一个协程函数,执行之后会获得一个generator,对generator执行send或者next才会开始真正的执行协程逻辑,从头或者从上次执行的地方一直直行到下一个yield,然后再挂起返回给外面。
python的生成器跟C++协程很像,都是无栈协程,但是C++的更复杂,因为C++是需要自定义协程对象。
例如这个简单的C++逻辑:
TaskCoroutine task_func() {
std::cout << "task first run" << std::endl;
co_yield 6;
std::cout << "---before await task2---" << std::endl;
co_await task2();
std::cout << "task resume" << std::endl;
co_return 3;
}
int main() {
std::cout << "Before task_func" << std::endl;
TaskCoroutine load_task = task_func();
std::cout << "After task_func" << std::endl;
load_task.resume();
std::cout << "After resume" << std::endl;
return 0;
}
这里因为有co_yield,所以task_func是一个协程函数,但是跟python不同,C++需要定义携程的返回类型,是一个自定义类型,代表的这个携程的Task或者Coroutine。
这个类型类型必须包含promise_type名字的PromiseType类,例如:
struct TaskCoroutine
{
using promise_type = PromiseType;
}
没有这个那编译就会异常,感觉这个是类似于C++20的概念约束做的。
promise_type又是另外一个约束的类,必须拥有几个必须定义的函数,以及几个可选定义的函数:
struct PromiseType {
PromiseType()
{
std::cout << "PromiseType" << std::endl;
}
TaskCoroutine get_return_object() {
std::cout << "get_return_object" << std::endl;
return TaskCoroutine{std::coroutine_handle<PromiseType>::from_promise(*this)};
}
std::suspend_never initial_suspend() noexcept {
std::cout << "initial_suspend" << std::endl;
return {};
}
Awaiter<true> final_suspend() noexcept {
std::cout << "final_suspend" << std::endl;
return {};
}
void unhandled_exception() {
std::cout << "unhandled_exception" << std::endl;
}
std::suspend_always yield_value(int x) noexcept {
std::cout << "yield_value" << std::endl;
return {};
}
void return_value(int x) noexcept {
std::cout << "return_value" << std::endl;
}
};
剩下的内容实在是不想写了,看我的视频吧:
https://www.bilibili.com/video/BV1H66aYTE84
这是《现代C++语言核心特性解析》的读书笔记。
书里给constexpr给了很多篇幅,但是总体来说就是一个,const表示的是不允许修改的常量,而constexpr表示的是编译期的常量。
但是逐渐被扩展到constexpr可以被用作函数,lambda,以及if constexprt,都是为了让编译期可以做更多的事情,给与开发者更多的自主性。
反正基本上想要去做一些预计算以及在模板里,用constexpr就没错了。
这件事情我理解来说就是constexpr被扩展定义之后的一个收缩,constexpr最初是为了定义编译期的常量,好让编译器在编译器做一些逻辑。但是逐渐扩展到函数也能放,同时运行期也能调用constexpr的函数。
那consteval做的事情就是,定义一个函数,这个函数只允许编译期运行。
说实话,有点过渡定义……
因为之前做一个东西遇到过类似的依赖问题,后来用了宏+一个很取巧的类构造的方式来保证顺序。
然后C++20提供了一个更简单的方法,constinit保证在编译期就会初始化对象,相当于是最早初始化的一批对象。
新标准还提供了一个新函数,用于区分当前逻辑运行在编译期还是运行时,可以用if constexpr去做不同的逻辑。
#include <cmath>
#include <type_traits>
constexpr double power(double b, int x) {
if (std::is_constant_evaluated() && x >= 0) {
double r = 1.0, p = b;
unsigned u = (unsigned)x;
while (u != 0) {
if (u & 1) r *= p;
u /= 2;
p *= p;
}
return r;
} else {
return std::pow(b, (double)x);
}
}
int main()
{
constexpr double kilo = power(10.0, 3); // 常量求值
int n = 3;
double mucho = power(10.0, n); // 非常量求值
return 0;
}
在python中是能够很轻易地实现多个返回值的函数的,例如:
def return_multi_values():
return 100, 20
x, y = return_multi_values()
但是在C++应该是会用类、结构体或者引用来实现,在C++11之后也引入了std::tuple,也可以用来实现类似的功能:
#include <iostream>
#include <tuple>
std::tuple<int, int> return_multiple_values()
{
return std::make_tuple(11, 7);
}
int main()
{
int x = 0, y = 0;
std::tie(x, y) = return_multiple_values();
std::cout << "x=" << x << " y=" << y << std::endl;
}
这里定义和获取都很麻烦:
auto return_multiple_values()
{
return std::make_tuple(11, 7);
}
第二个问题就需要使用C++17引入的新特性——结构化绑定。
所谓结构化绑定是指将一个或多个名称绑定到初始化对象中的一个或多个子对象(或者元素)上,相当于给初始化对象的子对象起了别名。这里引用和别名是有区别的。
使用结构化绑定的方式是 auto[xx, yy, zz] = xxxx,例如:
#include <iostream>
#include <tuple>
auto return_multiple_values()
{
return std::make_tuple(11, 7);
}
int main()
{
auto[x, y] = return_multiple_values();
std::cout << "x=" << x << " y=" << y << std::endl;
}
但右边的值也不一定非得是函数返回值或者tuple,合理的表达式也行:
#include <iostream>
#include <string>
struct BindTest {
int a = 42;
std::string b = "hello structured binding";
};
int main()
{
BindTest bt;
auto[x, y] = bt;
std::cout << "x=" << x << " y=" << y << std::endl;
}
在for循环里会有更好的效果:
#include <iostream>
#include <string>
#include <vector>
struct BindTest {
int a = 42;
std::string b = "hello structured binding";
};
int main()
{
std::vector<BindTest> bt{ {11, "hello"}, {7, "c++"}, {42, "world"} };
for (const auto& [x, y] : bt) {
std::cout << "x=" << x << " y=" << y << std::endl;
}
}
这两个理解是错误的:
在结构化绑定中编译器会根据限定符生成一个等号右边对象的匿名副本,而绑定的对象正是这个副本而非原对象本身。
auto [xx, yy, zz]是能够增加修饰符的,例如const,volatile等, 甚至&引用也可以,这些都会直接作用在新的变量上。
包括原生数组、结构体和类对象、元组和类元组的对象。
它是所有情况中最简单的,条件是别名数量和数组数量一致。
首先,类或者结构体中的非静态数据成员个数必须和标识符列表中的别名的个数相同;其次,这些数据成员必须是公有的(C++20标准修改了此项规则,详情见20.5节);这些数据成员必须是在同一个类或者基类中;最后,绑定的类和结构体中不能存在匿名联合体
类元祖对象的定义比较复杂,不像python有ABC这种元类,C++是通过模板实现判断,目标类型提供std::tuple_size、std::tuple_element以及get的特化或者偏特化版本即算是类元祖对象。
在标准库中,除了std::tuple,还有std::pair和std::array也满足这个条件,这就带来了一个很好的特性:能够在for循环遍历map的时候直接使用结构化绑定,例如:
#include <iostream>
#include <string>
#include <map>
int main()
{
std::map<int, std::string> id2str{ {1, "hello"}, {3, "Structured"}, {5, "bindings"} };
for (const auto& elem : id2str) {
std::cout << "id=" << elem.first
<< ", str=" << elem.second << std::endl;
}
}
End.
重写(override)、重载(overload)和隐藏(overwrite)在C++完全不同的概念,先梳理一下区别:
重写的意思更加接近于覆盖,在C++中是指派生类覆盖了基类的虚函数,这里的覆盖必须满足有相同的函数签名和返回类型,即重写是有相同的函数名、形参列表以及返回类型。
它通常指一个类中有两个或者以上的函数,他们函数名相同,但是函数签名不同。
隐藏的概念是指基类成员函数,无论是否是虚函数,当派生类出现同名函数时,如果派生类函数签名和基类不同,则基类的会被隐藏;如果派生类函数签名和基类相同,如果是虚函数则为重写,否则为隐藏。
如果想在子类中使用基类的函数,可以使用using关键字将其引入派生类。
重写容易出现问题,即基类定义了虚函数,但是子类的函数名写错了,也不会有编译错误,只有运行测试时才会发现错误。
因此C++引入了一个非常实用的关键字,即override,这个关键词告诉编译器,这个函数需要覆盖基类的虚函数,一旦编译器发现虚函数不符合重写规则,就会报错。
在C++引入了final关键词来阻止派生类继承虚函数。它告诉编译器,这个函数不能被重写,如果重写了会编译报错,跟override一样放在函数声明的尾部。
End.