基于Boost::asio的多线程异步TCP服务器

本文介绍了一个基于Boost::asio库的多线程异步TCP服务器实现,包括io_service线程池管理和自定义协议头处理。服务器能够处理大规模并发连接,支持文件传输,实现了异常回调和数据读写。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://www.cppblog.com/Solstice/default.html?page=6

https://blog.youkuaiyun.com/kongwei1234/article/details/104778613

http://swiftcode.a1feeds.com/United-States/NEW-YORK-NY/JP-MORGAN-CHASE-BANK-NA

基于Boost::asio的多线程异步TCP服务器,实现了io_service线程池,测试了1万左右的并发访问,读写无压力

boost库中的ASIO网络库是目前最流行的几大网络编程框架之一,能支持大规模并发服务器的设计需求,这个库还是很牛逼的。aiso是基于C++进行开发,包含了大量C++11新特性,用它进行服务器开发还是比较方便的,写起来语句也会比较简洁,实现起来比较容易。
博主去年粗略的学习了一下asio,看了网上的许多博客,但是由于对于C++11新特性不熟悉,因此之前一直没有很理解,当时也忙也没有时间系统的学习。最近疫情原因有了很多空闲时间,因此重新学习一下用asio网络库开发服务器。
本篇博客主要实现基于ASIO网络库的异步服务器,所有读写事件都是异步的,并实现了io_service线程池,每个io_service单独运行在一个线程中,通过线程池给每个连接进来的客户端分配io_service。
在用asio进行服务器开发的时候,我们一定要先搞清楚一个概念:
asio中的异步事件,都是通过io_service去管理,在调用run()后,io_service就会去处理当前io_service管理的任务队列中的任务。

代码如下:

#include<iostream>
#include <boost/asio.hpp>
#include <string>
#include <boost/unordered_map.hpp>
using namespace boost::asio::ip;
using namespace std;
#define SERVER_PORT 7000
#define DATA_MAX_LEN 1024;
//数据包头
struct NetPacketHeader
{
    char cFlag;   //数据头的标志——填充‘s’
    long nLenth; //要发送的数据大小 由于需要发送文件,因此nlenth值可能很大,所以用long
    char cDataType;//接收/发送的数据类型 a-文件列表 b-文件 c-数据文本
    char sFileFormat[4];//如果是文件,这里即为文件格式,如果是文本数据,这里用'\0'填充
    char cSendDc; // 校验码 服务器发包填充-"s"  客户端发包填充-"c"
};

class serviceThreadPool
{
    //管理io_service的线程池
    //1.初始化一定数量的io_service,用io_service::work阻塞住,让其不退出
    //2.让io_service运行在不同的线程里,每来一个客户端,分配一个io_service
    std::vector<std::shared_ptr<boost::asio::io_service>> m_ioServiceList;
    std::vector<std::shared_ptr<boost::asio::io_service::work>> m_workList;
    //std::vector<std::thread> m_threadList;
    int m_poolSize;
    int m_io_service_pos; //依次分配io_service
public:
    //在多核的机器上,线程池中的数量最好等于核数
    explicit serviceThreadPool(int pool_size = 4) :m_io_service_pos(0)
    {
        m_poolSize = pool_size > 0 ? pool_size : 1;
        for (int i = 0; i < m_poolSize; i++)
        {
            std::shared_ptr<boost::asio::io_service> io_service(new boost::asio::io_service);
            std::shared_ptr<boost::asio::io_service::work> work(new boost::asio::io_service::work(*io_service));
            m_ioServiceList.push_back(io_service);
            m_workList.push_back(work);
        }
    }
    void startPool()
    {
        for (int i = 0; i < m_poolSize; i++) {

            std::thread runThread([=] {
                m_ioServiceList[i]->run();
            });
            runThread.detach();
        }
    }
    void stop()
    {
        for (int i = 0; i < m_poolSize; i++)
        {
            m_ioServiceList[i]->stop();
        }
    }
    boost::asio::io_service& get_io_service()
    {
        //通过这个函数分配io_service
        boost::asio::io_service& io_service = *m_ioServiceList[m_io_service_pos++];
        if (m_io_service_pos == m_poolSize - 1)m_io_service_pos = 0;
        return io_service;
    }

};

class rwhandler
{

#define MSG_HEAD_SIZE 16  //包头大小
    //由于读写操作时,socket要与读写操作的递归函数进行绑定,
    //因此这里单独实现一个读写类,用来处理,每一个连接进来的客户端的读写事件
    tcp::socket m_socket;
    unsigned char *m_getData;
    int m_clientId;
    std::function<void(int)> m_callbackError; //异常回调函数
public:
    explicit rwhandler(boost::asio::io_service& ios) :m_socket(ios)
    {
        m_getData = new unsigned char[1024];
        printf("rwhandler create \n");
    }
    ~rwhandler() {
        delete[]m_getData;
        printf("rwhandler distroy \n");
    }
    tcp::socket& getSock()
    {
        return m_socket;
    }
    void read()
    {
        printf("read \n");
        //可以先解析包头,在异步读操作的完成事件中判断包头是否正确,正确则继续读包体
        //在读包体的异步完成事件中,继续this->read();
        boost::asio::async_read(m_socket, boost::asio::buffer(m_getData, MSG_HEAD_SIZE),
            [=](const boost::system::error_code& ec, size_t size)
        {
            if (ec != nullptr) {
                printf("read error! \n");
                handleError(ec);
                return;
            }
            //这里可以将数据通过回调函数或者信号与槽传递回应用层
            //cout<< string((char*)m_getData,MSG_HEAD_SIZE)<<endl;
            NetPacketHeader *p = (NetPacketHeader *)m_getData;
            printf(" %c %d %c %s %c %d \n", p->cFlag, p->nLenth, p->cDataType, p->sFileFormat, p->cSendDc, size);
            printf("client read thread id is: %d \n", std::this_thread::get_id());
            this->read();
        });
    }
    void setClientID(int id) {
        m_clientId = id;
    }
    int getClientID() {
        return m_clientId;
    }
    //应用层,也就是server中利用SetCallBackError设置执行异常回调的函数
    template<typename F>
    void SetCallBackError(F f) {
        m_callbackError = f;
    }
    void handleError(const boost::system::error_code& ec)
    {
        //这里可以针对不同的ec的错误码进行异常解析
        printf("error! error value is: %d  %s \n", ec.value(), ec.message());
        //异常码为2表示异步任务等待时远端关闭套接字
        if (ec.value() == 2) {
            //m_socket.close();
            boost::system::error_code ec2;
            m_socket.shutdown(tcp::socket::shutdown_send, ec2);
            m_socket.close(ec2);
            printf("clinet close the connected \n");
            if (m_callbackError)
                m_callbackError(m_clientId);
        }
    }
};


class server
{
#define MAX_CLINET_NUM 7000//最大连接数量
    //这里利用m_rwIdList去管理客户端的无序表的ID,每次有新客户端连接进来,就从m_rwIdList的头部取一个ID,并将该ID从m_rwIdList中删除
    //如果有客户端异常或者主动断开,则根据客户端ID去移除无序表中的rwhandle,然后将该ID放回m_rwIdList的尾部
public:
    explicit server(boost::asio::io_service& ios, short port, int serPoolSize = 4) :m_ios(ios)
        , m_acceptor(ios, tcp::endpoint(tcp::v4(), port)),
        m_rwIdList(MAX_CLINET_NUM), m_servicePool(serPoolSize)
    {
        int current = 0;
        //初始化m_rwIdList,产生MAX_CLINET_NUM数量的ID
        std::generate_n(m_rwIdList.begin(), MAX_CLINET_NUM, [&current] {return ++current; });
        m_servicePool.startPool(); //开启线程池
    }
    ~server()
    {
        printf("server distroy! \n");
    }
    void accept()
    {
        //这里不用智能指针,使用rwhandler rw;就会报“尝试引用已删除对象”的错误
        //智能指针在离开作用于时应该也是会析构,为什么不会报错呢?
        //是因为在async_accept的lambda中将rw当作形参传入,这时候rw的引用计数就加1了,因此不会析构
        //将智能指针放入容器后,智能指引用计数就会加1
        std::shared_ptr<rwhandler> rw = std::make_shared<rwhandler>(m_servicePool.get_io_service());
        rw->setClientID(m_rwIdList.front());  //设置client ID
        m_rwIdList.pop_front();
        //设置异常回调函数
        rw->SetCallBackError([this](int id) {
            printf("start callback!  id:%d \n", id);
            printf("client callback thread id is:%d \n", std::this_thread::get_id());
            reSetRwID(id);
        });
        printf(" id:%d \n", rw->getClientID());
        printf("start accept  thread id : %d  \n", std::this_thread::get_id());
        //这里捕获列表,必须捕获rw,否则智能指针就会自动析构
        m_acceptor.async_accept(rw->getSock(), [this, rw](const boost::system::error_code& error) {
            if (error)
            {
                printf("------------------------acceptor error--------------------------------------");
                printf("error! error value is: %d  %s \n", error.value(), error.message());
                return;
            }
            
            printf("a client coming in! \n");
            printf("client adress is: %s  id: %d \n", rw->getSock().remote_endpoint().address(), rw->getClientID());
            //m_rwList.push_back(rw); //果然,这里在注释掉push_back操作后,智能指针随后就析构了
            {
                std::unique_lock<std::mutex> lock(this->m_lock);
                m_rwMap.insert(std::make_pair(rw->getClientID(), rw));
                printf("map size : %d \n", m_rwMap.size());
            }
            m_rwMap.insert(std::make_pair(rw->getClientID(), rw));
            printf("map size : %d \n", m_rwMap.size());
            printf("client have acceptor thread id is: %d \n", std::this_thread::get_id());
            rw->read();
            accept();
        });
    }

    void reSetRwID(int id)
    {
        std::unique_lock<std::mutex> lock(this->m_lock);
        auto it = m_rwMap.find(id);
        if (it != m_rwMap.end())
            m_rwMap.erase(it);
        m_rwIdList.push_back(id);
        printf("--------map size is: %d \n--------", m_rwMap.size());
    }
    size_t getClientNum() {
        return  m_rwMap.size();
    }

private:
    //std::vector<std::shared_ptr<rwhandler>> m_rwList;
    //无序表,用于管理连接来的客户端的读写
    boost::unordered_map<int, std::shared_ptr<rwhandler>> m_rwMap;
    std::list<int> m_rwIdList; //管理连接进来的客户端的ID
    boost::asio::io_service& m_ios; //这个io_service只用于acceptor
    tcp::acceptor m_acceptor;
    serviceThreadPool m_servicePool;
    std::mutex m_lock;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
使用方法:

int main()
{    
    boost::asio::io_service ios;
    server ser(ios, SERVER_PORT);
    ser.accept();
    ios.run();

    system("pause");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
其中大家一起要注意,所有对m_rwMap进行操作的地方一定要加锁,因为异常回调函数时运行在读写事件的线程中的,不同的io_service运行在不同的线程中,因此不同io_service下的读写事件也是运行在不同的线程中,所以m_rwMap可能会被不同的线程进行访问。博主刚开始就忽略了这一点,因此测试的时候总是异常,调试了一下午才发现问题,太难受了!

博主在写这个服务器时,是想用它代替FTP服务器进行文件传输,因此,这里实现了自定义协议头,这里的协议头只为了测试,因此比较粗糙。这里再解析时,在读取到协议头大小的数据后,只对数据进行了打印,正常我们项目中,这里需要先解析协议头,再根据协议头中的包体大小对包体进行读取,读取到的数据可以利用回调函数回调到应用层(数据回调的方法这里没有实现,但是实现了异常回调,方法都一样,大家可以参考)。
这个代码基本的框架都写好了,但只是框架,其中细节部分还需要大家根据自己的需求进行修改添加即可。其中接收到协议头后,可以根据协议头读取包体,然后用异步写的方法回复数据包。


————————————————
版权声明:本文为优快云博主「E404」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/kongwei1234/article/details/104778613

<think>我们正在处理一个关于在多线程环境下使用boost::asio和sdbusplus的问题。用户要求提供使用指南。根据提供的引用,我们可以总结出以下几点: 1. 引用[1]展示了如何使用boost::asio创建TCP端点(endpoint)和socket。 2. 引用[2]讨论了多线程环境下使用io_context的两种方式:一种是多个线程共享一个io_context,并使用strand来保证处理程序的顺序执行;另一种是在主线程运行io_context,而在其他线程中投递异步操作,此时需要注意线程安全(使用原子操作等)。 3. 引用[3]提到,boost::asio中的对象(如定时器)与特定的io_context关联,因此在创建这些对象时需要传入io_context。 另外,sdbusplus是一个基于sd-bus的C++封装库,用于D-Bus通信。它本身是线程安全的吗?通常,D-Bus库要求连接(connection)在同一时间只能被一个线程使用,或者需要显式地启用多线程支持并处理锁。 结合boost::asio和sdbusplus在多线程环境下的使用,我们可能需要考虑以下方面: - 如何设置boost::asio的io_context在多线程环境下运行? - 如何确保sdbusplus的D-Bus连接在多线程中安全使用? - 如何将boost::asio的事件循环与sdbusplus的事件循环整合(如果需要)? 由于sdbusplus有自己的事件循环(通常基于sd-bus的事件循环),而boost::asio也有自己的事件循环(io_context),我们可能需要在一个线程中同时处理两个事件循环,或者将两个事件循环整合。但更常见的做法是让它们分别运行在不同的线程中,并通过线程安全的方式通信。 然而,用户的问题集中在“多线程使用指南”,因此我们将重点放在如何安全地在多线程中使用这两个库。 ### 多线程使用boost::asio 根据boost::asio的文档,有以下几种多线程模型: 1. **单io_context,多线程运行io_context::run**:多个线程同时调用同一个io_context的run方法。这样,完成处理程序(completion handlers)可能会被并发执行。因此,对于共享资源,需要使用锁(如互斥量)或strand(一种特殊的执行器,用于确保处理程序按顺序执行)来保证线程安全。 ```cpp boost::asio::io_context io_context; // ... 将一些异步操作投递到io_context中 // 创建多个线程运行io_context std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back([&io_context]() { io_context.run(); }); } // 等待所有线程结束 for (auto& t : threads) { t.join(); } ``` 当使用这种方式时,如果多个处理程序访问共享资源,应该使用strand来包装: ```cpp boost::asio::io_context::strand my_strand(io_context); // 然后使用strand的wrap函数来包装处理程序,确保这些处理程序不会并发执行 my_socket.async_read_some(..., my_strand.wrap([](auto...){ ... })); ``` 2. **每个线程一个io_context**:每个线程有自己的io_context,这样每个io_context都是独立运行的。这种模型通常用于将特定的资源绑定到特定的线程(例如,每个连接绑定一个线程)。这种方式避免了不同线程间的资源竞争,但线程间通信需要额外的机制(如队列)。 ### 多线程使用sdbusplus sdbusplus的D-Bus连接(sdbusplus::bus_t)通常不是线程安全的。根据sd-bus的文档,一个总线连接(bus connection)不应该在多个线程中同时使用,除非显式地启用多线程支持并加锁。但是,sdbusplus本身并没有提供内置的多线程支持。因此,推荐的做法是: - 每个线程使用自己的bus_t对象(即每个线程创建自己的D-Bus连接),或者 - 在多个线程间共享一个bus_t对象,但必须确保同一时间只有一个线程在调用该对象的方法(通过将所有的D-Bus操作分派到同一个io_context的strand中执行,或者使用一个专门的线程来处理D-Bus连接)。 然而,创建多个D-Bus连接可能会增加系统负担,并且可能不是所有情况下都允许(例如,某些总线只能有一个连接)。 因此,更常见的做法是使用一个单独的线程来处理D-Bus连接,然后通过消息队列或其他线程间通信机制将请求从其他线程传递给这个专用线程。 ### 结合boost::asio和sdbusplus 如果我们希望在一个应用程序中同时使用boost::asio和sdbusplus,并且希望它们都能在多线程环境下工作,我们可以考虑以下架构: 1. 创建一个io_context对象,用于处理所有的异步I/O(如网络、定时器等)。 2. 创建一个sdbusplus::bus_t对象,用于D-Bus通信。 3. 将sdbusplus::bus_t与boost::asio整合:sdbusplus的bus_t可以获取底层的文件描述符(通过`int get_fd()`),然后我们可以将这个文件描述符注册到boost::asio的io_context中,以便在D-Bus有事件时通知asio,然后我们可以在asio的事件循环中处理D-Bus事件。 具体步骤: - 获取bus_t的文件描述符:`int fd = bus.get_fd();` - 创建一个boost::asio::posix::stream_descriptor对象,将其关联到io_context和这个文件描述符。 - 使用async_wait或async_read_some来监视这个文件描述符的可读事件。当可读时,调用`bus.process()`来处理D-Bus事件(注意:`bus.process()`处理一个事件,然后返回,因此可能需要循环处理直到没有事件)。 - 重复注册异步读取。 这样,我们就可以在boost::asio的事件循环中处理D-Bus事件。 4. 多线程处理:我们可以使用多个线程运行io_context的run方法。但是,由于D-Bus连接(bus_t)不是线程安全的,我们需要确保所有对bus_t的操作(包括`process()`和任何发送消息的方法)都在同一个strand中执行,或者在一个线程中执行(即只在一个线程中运行io_context,而其他线程通过投递任务到该io_context来间接操作bus_t)。 ### 示例代码框架 下面是一个简化的示例,展示如何将sdbusplus::bus_t整合到boost::asio的事件循环中,并在多线程环境下运行(但确保bus_t操作在一个strand中执行): ```cpp #include <boost/asio.hpp> #include <sdbusplus/asio/connection.hpp> #include <sdbusplus/bus.hpp> #include <thread> int main() { boost::asio::io_context io_context; // 创建sdbusplus的bus对象,使用asio的封装:sdbusplus::asio::connection // 注意:sdbusplus::asio::connection已经封装了与asio的整合 auto bus = std::make_shared<sdbusplus::asio::connection>(io_context); // 使用bus对象注册D-Bus对象和方法... // 创建strand,用于保护对bus的操作(实际上,sdbusplus::asio::connection内部可能已经做了保护,但为了安全,我们仍然使用strand) auto strand = std::make_shared<boost::asio::io_context::strand>(io_context); // 在多个线程中运行io_context std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back([&io_context]() { io_context.run(); }); } // 在主线程中,我们可以投递一些操作到strand中,这些操作将安全地访问bus boost::asio::post(*strand, [bus]() { // 在strand中安全地使用bus // 例如:调用D-Bus方法 }); // ... 其他操作 // 等待所有线程结束 for (auto& t : threads) { t.join(); } return 0; } ``` 注意:sdbusplus库中有一个`asio`子目录,其中提供了`connection`类,这个类已经封装了与boost::asio的整合。因此,我们不需要手动注册文件描述符到asio,因为`connection`内部已经处理了。 ### 总结 在多线程环境下使用boost::asio和sdbusplus,关键点在于: - 使用boost::asio的io_context在多线程中运行(多个线程调用run),并使用strand来保证对共享资源(包括sdbusplus的bus对象)的访问是线程安全的。 - 使用`sdbusplus::asio::connection`来创建bus连接,它已经与boost::asio整合,因此可以直接在asio的事件循环中处理D-Bus事件。 - 所有对bus对象的操作(包括调用D-Bus方法、处理信号等)都应该通过同一个strand来投递,以确保这些操作不会并发执行。 这样,我们就可以安全地在多线程应用中使用boost::asio和sdbusplus了。 ### 相关问题 1. 如何将sdbusplus的D-Bus事件循环整合到boost::asio中? 2. 在多线程中使用sdbusplus的bus_t对象有哪些注意事项? 3. 除了使用strand,还有哪些方法可以保证boost::asio处理程序的线程安全?</think>### 多线程环境下使用 Boost.Asio 和 sdbusplus 指南 #### 一、Boost.Asio 多线程核心机制 1. **io_context 多线程模型** - **共享 io_context**:多个线程共同执行 `io_context::run()`,需配合 `strand` 保证处理程序顺序执行 ```cpp boost::asio::io_context io_ctx; boost::asio::io_context::strand my_strand(io_ctx); // 创建strand[^2] // 线程池运行io_context std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back([&io_ctx] { io_ctx.run(); }); } ``` - **独立 io_context**:每个线程拥有专属 `io_context`,避免资源竞争 ```cpp std::vector<std::unique_ptr<boost::asio::io_context>> io_contexts; for (int i = 0; i < 4; ++i) { io_contexts.push_back(std::make_unique<boost::asio::io_context>()); std::thread([ctx=io_contexts.back().get()] { ctx->run(); }).detach(); } ``` 2. **线程安全对象绑定** 需确保异步对象(如 `socket`/`timer`)与特定 `io_context` 关联[^3]: ```cpp boost::asio::steady_timer timer(io_ctx); // 绑定到io_context timer.expires_after(std::chrono::seconds(1)); timer.async_wait([](auto...){ /* 处理程序 */ }); ``` #### 二、sdbusplus 多线程实践 1. **D-Bus 连接管理** - **单连接多线程**:使用 `sdbusplus::asio::connection` 封装,通过 `strand` 串行化访问 ```cpp auto bus = std::make_shared<sdbusplus::asio::connection>(io_ctx); boost::asio::post(my_strand, [bus] { auto method = bus->new_method_call("service", "/path", "interface", "method"); method.append(/* 参数 */); bus->call(method); // 安全调用 }); ``` - **多连接独立线程**:每个线程创建专属 D-Bus 连接 ```cpp void worker_thread() { sdbusplus::asio::connection local_bus(io_ctx_local); // 独立使用local_bus } ``` 2. **信号处理同步** 使用 `asio::signal_set` 绑定到 `strand`: ```cpp boost::asio::signal_set signals(io_ctx, SIGINT, SIGTERM); signals.async_wait(my_strand.wrap([](auto, auto){ /* 安全处理信号 */ })); ``` #### 三、整合架构示例 ```mermaid graph TD A[主线程] -->|创建| B[io_context] A -->|创建| C[sdbusplus::connection] B --> D[线程池 run] C -->|绑定| B D -->|strand 封装| E[DBus 方法调用] D -->|strand 封装| F[定时器操作] D -->|strand 封装| G[信号处理] ``` #### 四、关键注意事项 1. **共享资源保护** - 跨线程数据访问需使用 `std::atomic` 或互斥锁 - DBus 对象路径注册需在同一个线程完成 2. **性能优化** - I/O 密集型任务:共享 `io_context` + `strand` 分组[^2] - 计算密集型任务:独立 `io_context` 隔离负载 3. **错误处理** ```cpp timer.async_wait(my_strand.wrap([](boost::system::error_code ec) { if (ec) { // 统一错误处理路径 } })); ``` > **最佳实践**:对于DBus代理对象,推荐使用 `sdbusplus::asio::proxy` 自动匹配 `io_context` 和 `strand`,简化线程安全管理。 --- ### 相关问题 1. 如何避免 `io_context` 在多线程中的资源竞争问题? 2. `sdbusplus::asio::connection` 在哪些场景下需要手动加锁? 3. 如何设计超时机制整合 Boost.Asio 定时器和 D-Bus 方法调用? 4. 多线程环境下 D-Bus 信号订阅有哪些特殊注意事项? [^1]: 创建socket时需绑定io_context [^2]: 多线程分组需使用strand保证顺序 [^3]: 定时器等对象必须关联特定io_context
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值