thread_info_base
干什么的
- 是一个
per-thread(每线程)内存池 / 分配器缓存,核心目的是:减少异步操作中反复new/delete的开销
背景
ASIO里每次调用async_read、async_write之类的操作,内部都需要分配一小块内存来保存"这个异步操作的状态"(handler、缓冲区信息等)- 如果每次都走系统的
malloc/free,在高并发、高频率的场景下会成为性能瓶颈 ASIO设计了一个轻量级、每线程独立的内存复用机制,避免频繁调用系统分配器,同时天然线程安全(因为每个线程只碰自己的缓存,不需要加锁)
标签系统
- 这些
tag把内存缓存分成了不同"用途"的区域,每个tag通过begin_mem_index/end_mem_index圈定自己在reusable_memory_数组里的一段槽位- 比如协程帧分配(
awaitable_frame_tag)和executor函数分配(executor_function_tag)各自用不同的槽位,互不干扰
- 比如协程帧分配(
|
1 2 3 4 5 6 |
struct default_tag { ... }; struct awaitable_frame_tag { ... }; struct executor_function_tag { ... }; struct cancellation_signal_tag { ... }; struct parallel_group_tag { ... }; struct timed_cancel_tag { ... }; |
- 这样设计是因为不同用途的内存块大小、生命周期模式不一样,分开管理复用效率更高
allocate 核心逻辑
- 这是一个很简单的"单槽位缓存"思路,本质是懒惰复用:
- 分配时:先看当前线程的缓存槽里有没有大小合适、对齐满足要求的空闲内存块,有就直接复用,不用真的调用系统分配器
- 没有合适的,就调
aligned_new真正分配一块 - 释放时:不是真的还给系统,而是先塞回线程自己的缓存槽里,留着下次用;槽位满了才真正
aligned_delete
- 它用
chunks(把大小换算成固定粒度的"块数")加上一个trick- 在内存块末尾多留一个字节存
chunks数值 - 来快速判断某块缓存是否够大,不需要额外的元数据结构
- 在内存块末尾多留一个字节存
|
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 |
template <typename Purpose> static void* allocate(Purpose, thread_info_base* this_thread, std::size_t size, std::size_t align = ASIO_DEFAULT_ALIGN) { std::size_t chunks = (size + chunk_size - 1) / chunk_size; if (this_thread) { for (int mem_index = Purpose::begin_mem_index; mem_index < Purpose::end_mem_index; ++mem_index) { if (this_thread->reusable_memory_[mem_index]) { void* const pointer = this_thread->reusable_memory_[mem_index]; unsigned char* const mem = static_cast<unsigned char*>(pointer); if (static_cast<std::size_t>(mem[0]) >= chunks && reinterpret_cast<std::size_t>(pointer) % align == 0) { this_thread->reusable_memory_[mem_index] = 0; mem[size] = mem[0]; return pointer; } } } for (int mem_index = Purpose::begin_mem_index; mem_index < Purpose::end_mem_index; ++mem_index) { if (this_thread->reusable_memory_[mem_index]) { void* const pointer = this_thread->reusable_memory_[mem_index]; this_thread->reusable_memory_[mem_index] = 0; aligned_delete(pointer); break; } } } void* const pointer = aligned_new(align, chunks * chunk_size + 1); unsigned char* const mem = static_cast<unsigned char*>(pointer); mem[size] = (chunks <= UCHAR_MAX) ? static_cast<unsigned char>(chunks) : 0; return pointer; } |
allocate 深入
- 先把请求的字节数
size换算成"块数"(chunk_size通常是4或8字节)- 这是向上取整的写法,比如
size=10,chunk_size=4,chunks = (10+3)/4 = 3 - 用块数而不是精确字节数来比较,是为了让"稍大一点的旧块"也能被复用,不用精确匹配大小
- 这是向上取整的写法,比如
|
1 |
std::size_t chunks = (size + chunk_size - 1) / chunk_size; |
- 然后逻辑分三步:
- 在缓存里找可复用的
- 找不到就顺手清理一个不合适的
- 实在没有就真的分配新内存
- 详细如下
- 扫描复用
- 只在这个
Purpose(比如awaitable_frame_tag)自己的槽位范围内找 - 关键在
mem[0]:每个空闲块闲置时,会把自己一共有多少个chunk存在第0个字节里 - 所以这里判断
mem[0] >= chunks,意思是"这块旧内存够不够大,能不能装下这次请求",同时还要检查对齐(pointer % align == 0)
因为不同请求可能要求不同的对齐方式,旧块的起始地址未必满足 - 一旦找到合适的块:
把这个槽位清空(reusable_memory_[mem_index] = 0),表示"取走了,不再闲置" - 关键的一步:
mem[size] = mem[0],把"这块内存有多少chunk"的记录,从索引0挪到索引size(也就是这次请求的字节数末尾)的位置
这是为了配合deallocate时的写法,因为块被再次使用时,未来释放它要基于这次的size才能找到记录
- 只在这个
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
for (int mem_index = Purpose::begin_mem_index; mem_index < Purpose::end_mem_index; ++mem_index) { if (this_thread->reusable_memory_[mem_index]) { void* const pointer = this_thread->reusable_memory_[mem_index]; unsigned char* const mem = static_cast<unsigned char*>(pointer); if (static_cast<std::size_t>(mem[0]) >= chunks && reinterpret_cast<std::size_t>(pointer) % align == 0) { this_thread->reusable_memory_[mem_index] = 0; mem[size] = mem[0]; return pointer; } } } |
- 找不到合适的块时,顺手清理一个
- 如果上面扫了一圈都没找到"大小够 + 对齐对"的块,说明缓存里现有的块都不合适(可能太小,或者残留着不常用大小的旧块占着槽位)
- 这里会真正释放掉一个槽位里闲置的块(只删一个,
break立刻退出),腾出空间 - 这不是为了给这次分配让路(反正马上就要走系统分配器分配新内存了)
而是一种渐进式的缓存清理:避免槽位一直被"用不上的尺寸"占满,导致真正该被复用的内存进不来
|
1 2 3 4 5 6 7 8 9 10 |
for (int mem_index = Purpose::begin_mem_index; mem_index < Purpose::end_mem_index; ++mem_index) { if (this_thread->reusable_memory_[mem_index]) { void* const pointer = this_thread->reusable_memory_[mem_index]; this_thread->reusable_memory_[mem_index] = 0; aligned_delete(pointer); break; } } |
- 真正分配新内存
- 分配的大小是
chunks * chunk_size + 1——多分配了 1 个字节 - 这多出来的
1字节就是用来存"这块内存有多少个chunk"的自描述信息,写在mem[size]位置 - 如果
chunks超过了unsigned char能表示的最大值(255),就存0,表示"太大了,记不下精确值" - 这样以后这块内存被释放缓存起来时也不会被
deallocate里那个size <= chunk_size * UCHAR_MAX的判断放进缓存(直接会走aligned_delete,因为太大的块反复缓存意义不大,还占地方)
- 分配的大小是
|
1 2 3 4 |
void* const pointer = aligned_new(align, chunks * chunk_size + 1); unsigned char* const mem = static_cast<unsigned char*>(pointer); mem[size] = (chunks <= UCHAR_MAX) ? static_cast<unsigned char>(chunks) : 0; return pointer; |
内存对齐分配
|
1 2 3 4 5 6 7 8 9 |
align = (align < ASIO_DEFAULT_ALIGN) ? ASIO_DEFAULT_ALIGN : align; size = (size % align == 0) ? size : size + (align - size % align); void* ptr = _aligned_malloc(size, align); if (!ptr) { std::bad_alloc ex; asio::detail::throw_exception(ex); } return ptr; |
deallocate 核心逻辑
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
template <typename Purpose> static void deallocate(Purpose, thread_info_base* this_thread, void* pointer, std::size_t size) { if (size <= chunk_size * UCHAR_MAX) { if (this_thread) { for (int mem_index = Purpose::begin_mem_index; mem_index < Purpose::end_mem_index; ++mem_index) { if (this_thread->reusable_memory_[mem_index] == 0) { unsigned char* const mem = static_cast<unsigned char*>(pointer); mem[0] = mem[size]; this_thread->reusable_memory_[mem_index] = pointer; return; } } } } aligned_delete(pointer); } |
deallocate深入
- 这块内存值不值得缓存
UCHAR_MAX是255(unsigned char能表示的最大值)- 这个判断的意思是:只有当这次释放的内存大小换算成
chunk数之后,能用1个字节记录下来(即chunks ≤ 255),才有资格进缓存池 - 如果这次释放的内存太大(
chunks超过255),就直接判定不缓存,走最下面的aligned_delete,不进入下面的逻辑
|
1 |
if (size <= chunk_size * UCHAR_MAX) |
- 找一个空槽位塞进去
- 只在这个
Purpose对应的槽位区间(begin_mem_index到end_mem_index)里找 - 只要找到第一个空槽位(值为
0,即没有缓存任何指针),就把这块要释放的内存塞进去,然后立刻return,不会真的调用系统释放 - 关键的一行是:
mem[0] = mem[size];
回忆一下:这块内存当初被分配(或者上次被复用)时,chunk数记在mem[size]位置
现在要把它放进"闲置状态",闲置状态下统一约定"标记存在索引0"的位置
- 只在这个
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if (this_thread) { for (int mem_index = Purpose::begin_mem_index; mem_index < Purpose::end_mem_index; ++mem_index) { if (this_thread->reusable_memory_[mem_index] == 0) { unsigned char* const mem = static_cast<unsigned char*>(pointer); mem[0] = mem[size]; this_thread->reusable_memory_[mem_index] = pointer; return; } } } |
- 兜底,真正释放
- 这次释放的内存太大(
size > chunk_size * UCHAR_MAX),一开始就不进缓存逻辑 this_thread为空(比如不在受管理的线程上),或者该Purpose对应的槽位已经全部被占满,没地方放
- 这次释放的内存太大(
|
1 |
aligned_delete(pointer); |
总结
thread_info_base就存了当前线程的复用内存,以及异常信息- 而
io_context::run()跑在哪个线程,当前线程就是哪个线程
|
1 2 3 |
struct win_iocp_thread_info : public thread_info_base { }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
size_t win_iocp_io_context::run(asio::error_code& ec) { if (::InterlockedExchangeAdd(&outstanding_work_, 0) == 0) { stop(); ec = asio::error_code(); return 0; } win_iocp_thread_info this_thread; thread_call_stack::context ctx(this, this_thread); size_t n = 0; while (do_one(INFINITE, this_thread, ec)) if (n != (std::numeric_limits<size_t>::max)()) ++n; return n; } |
thread_context
win_iocp_io_context继承了thread_context
|
1 2 3 4 5 6 7 8 9 10 11 |
class thread_context { public: // Obtain a pointer to the top of the thread call stack. Returns null when // not running inside a thread context. ASIO_DECL static thread_info_base* top_of_thread_call_stack(); protected: // Per-thread call stack to track the state of each thread in the context. typedef call_stack<thread_context, thread_info_base> thread_call_stack; }; |
thread_call_stack类型- 定义用
thread_context和thread_info_base对call_stack的物化类型
- 定义用
call_stack
tss_ptr<context>
ASIO自己封装的线程本地存储指针- 每个线程看到的
top_都是独立的一份,这是整个类线程安全、不用加锁的根本原因
- 每个线程看到的
|
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 |
template <typename T> class win_tss_ptr : private noncopyable { public: // Constructor. win_tss_ptr() : tss_key_(win_tss_ptr_create()) { } // Destructor. ~win_tss_ptr() { ::TlsFree(tss_key_); } // Get the value. operator T*() const { return static_cast<T*>(::TlsGetValue(tss_key_)); } // Set the value. void operator=(T* value) { ::TlsSetValue(tss_key_, value); } private: // Thread-specific storage to allow unlocked access to determine whether a // thread is a member of the pool. DWORD tss_key_; }; |
tss_key_是一个DWORD,本质上是一个"索引/句柄",不是真正的数据- 它的作用类似于"一个槽位编号"
- 操作系统会为每个线程维护一份独立的
TLS数据区,tss_key_就是"到这个数据区里第几号槽位去存/取值"的编号,所有线程共用同一个编号,但各自槽位里的内容互不干扰
|
1 2 3 4 |
win_tss_ptr() : tss_key_(win_tss_ptr_create()) { } |
- 读取值(隐式转换运算符)
::TlsGetValue(tss_key_)是Win32 API,语义是"取出当前调用线程在这个槽位编号下存的值"- 虽然
tss_key_这个编号是所有线程共享的,但TlsGetValue返回的内容是每个线程各自独立存的那一份,操作系统在背后帮你按"当前是哪个线程在调用"做了区分
|
1 2 3 4 |
operator T*() const { return static_cast<T*>(::TlsGetValue(tss_key_)); } |
|
1 2 3 |
tss_ptr<context> top_; context* c = top_; // 自动调用 operator T*(),取出"当前线程"存的值 if (top_) { ... } // 同样会隐式转换 |
call_stack/context
|
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 |
class call_stack { public: // Context class automatically pushes the key/value pair on to the stack. class context : private noncopyable { public: // Push the key on to the stack. explicit context(Key* k) : key_(k), next_(call_stack<Key, Value>::top_) { value_ = reinterpret_cast<unsigned char*>(this); call_stack<Key, Value>::top_ = this; } // Push the key/value pair on to the stack. context(Key* k, Value& v) : key_(k), value_(&v), next_(call_stack<Key, Value>::top_) { call_stack<Key, Value>::top_ = this; } // Pop the key/value pair from the stack. ~context() { call_stack<Key, Value>::top_ = next_; } // Find the next context with the same key. Value* next_by_key() const { context* elem = next_; while (elem) { if (elem->key_ == key_) return elem->value_; elem = elem->next_; } return 0; } private: friend class call_stack<Key, Value>; // The key associated with the context. Key* key_; // The value associated with the context. Value* value_; // The next element in the stack. context* next_; }; friend class context; // Determine whether the specified owner is on the stack. Returns address of // key if present, 0 otherwise. static Value* contains(Key* k) { context* elem = top_; while (elem) { if (elem->key_ == k) return elem->value_; elem = elem->next_; } return 0; } // Obtain the value at the top of the stack. static Value* top() { context* elem = top_; return elem ? elem->value_ : 0; } private: // The top of the stack of calls for the current thread. static tss_ptr<context> top_; }; template <typename Key, typename Value> tss_ptr<typename call_stack<Key, Value>::context> call_stack<Key, Value>::top_; |
- 在当前函数的栈上创建一个
win_iocp_thread_info对象thread_info_base,装着这个线程自己的内存复用缓存、异常暂存等信息- 注意它是局部变量,生命周期跟着这个函数走
this作为Key*(这里的this是外层调用这段代码的对象,是当前的win_iocp_io_context实例本身),this_thread作为Value&。这行代码执行完,效果是:- 把当前正在跑的
io_context和这个线程自己的win_iocp_thread_info关联到了一起 - 这个关联被压进了
call_stack<io_context, win_iocp_thread_info>::top_这个线程本地栈的栈顶 ctx这个局部变量的生命周期结束时(比如这个函数返回,或者这个作用域结束),析构函数自动把top_还原,等于把这个关联从栈里弹出去
- 把当前正在跑的
|
1 2 |
win_iocp_thread_info this_thread; thread_call_stack::context ctx(this, this_thread); |
- 总结来说
- 从现在开始,直到这个作用域结束,当前线程正在为这个
io_context(this)工作,需要用的线程私有数据是this_thread这份 - 之后在这条调用链的任何更深层代码里,只要想知道"我现在是不是在为某个
io_context服务?用的是哪份线程私有数据?",都可以通过call_stack::top()或call_stack::contains(key)查出来,不需要把this_thread这个指针一层层地当参数往下传
- 从现在开始,直到这个作用域结束,当前线程正在为这个
- 防止同一个
io_context在同一线程里递归调用自己导致死锁
|
1 2 3 4 5 6 7 8 9 10 11 12 |
void some_internal_function(io_context* ioc) { if (call_stack<io_context>::contains(ioc)) { // 说明当前线程的调用链上,已经在跑这个 io_context 了 // 可以选择直接内联执行任务,而不是重新排队等待(否则会自己等自己,死锁) } else { // 当前线程还没有在为这个 io_context 服务,走正常排队逻辑 } } |
整体设计思路
- 每个线程维护一个"调用链栈"
- 这个类要解决的问题是:
- 在当前这个线程的调用链条上,能不能找到某个特定"身份标识"(
Key)关联的数据?
- 在当前这个线程的调用链条上,能不能找到某个特定"身份标识"(
- 举个具体场景:
ASIO里有个经典用法是防止同一个io_context在同一线程里被递归调用导致死锁或重复执行- 比如线程正在执行
io_context::run()内部的某个handler,这个handler里又不小心间接调用了同一个io_context的东西 - 用
call_stack<io_context>就能在运行时检测出"当前线程的调用链上,是不是已经有这个io_context了",从而做出正确处理(比如直接内联执行而不是重新排队,或是避免死锁)
内联执行 vs 排队执行
排队执行(正常异步流程)
- 把
handler(回调函数)包装一下,塞进io_context的任务队列里,然后当前函数直接返回,不等handler真的执行完 - 等到某个线程(可能是当前线程,也可能是另一个线程)调用
run()/poll()从队列里取出这个任务时,才会真正调用它
内联执行(inline execution)
- 不经过队列,直接在当前这个函数调用的位置,同步地、立刻把
handler调用一遍,跟普通函数调用没有区别 - 等
handler执行完,some_internal_function才继续往下走或返回
为什么要区分这两种情况
- 假设你在一个正在被
io_context::run()调用的handler内部,又调用了io_context::dispatch(some_handler):
|
1 2 3 4 5 6 7 8 |
io.dispatch([]{ std::cout << "handler A executing\n"; io.dispatch([]{ std::cout << "handler B executing\n"; // 这行什么时候执行? }); std::cout << "handler A continuing\n"; }); io.run(); |
- 如果这里排队(像
post那样),handler B会被塞进队列,要等handler A彻底执行完、run()的循环再转一圈才会被取出执行。输出顺序是:
|
1 2 3 |
handler A executing handler A continuing handler B executing |
- 但如果
dispatch检测到"当前线程已经在这个io_context的调用链里了",就会内联执行——直接在原地把handler B调用掉,不排队。输出顺序变成:
|
1 2 3 |
handler A executing handler B executing handler A continuing |
- 这就是
dispatch语义上的承诺- 如果当前线程已经在为这个
io_context服务,就"尽快"执行(能立刻跑就立刻跑,不用等排队轮到) - 如果当前线程还没参与,就老老实实排队,转交给合适的线程去跑
- 如果当前线程已经在为这个
代码层面怎么实现"内联执行"
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void io_context::dispatch(Handler handler) { if (call_stack<io_context>::contains(this)) { // 内联执行:就是直接调用它,跟普通函数调用一样 handler(); } else { // 排队执行:包装后扔进任务队列,稍后由某个 run() 循环取出调用 post(handler); } } |
post vs dispatch
post
- 无条件排队,保证异步
- 调用它的这次函数调用一定在
handler真正执行之前就返回
- 调用它的这次函数调用一定在
- 无条件地把任务包装一下,塞进
io_context内部维护的一个任务队列(准确说是operation队列,里面存的不是原始的handler,而是包装过的、统一接口的"待执行操作"对象),然后由某个调用了run()/poll()的线程从队列里取出来执行
dispatch
- 条件允许内联
- 如果条件满足,直接在当前调用栈里同步执行
- 不满足则退化为跟
post一样的排队行为
dispatch不总是走队列dispatch在满足"内联条件"(当前线程已经身处这个执行上下文的调用链)时,是完全绕过队列的
run() 循环之外调用
- 这里
dispatch也没有内联执行- 因为调用
dispatch的时候,当前线程(main线程)还没有进入io_context的执行上下文(run()还没开始跑),call_stack::contains检测不到,所以dispatch退化成了排队
- 因为调用
|
1 2 3 4 5 6 7 8 9 10 11 |
asio::io_context io; std::cout << "1: before\n"; io.post([]{ std::cout << "2: post handler\n"; }); std::cout << "3: after post\n"; io.dispatch([]{ std::cout << "4: dispatch handler\n"; }); std::cout << "5: after dispatch\n"; io.run(); |
|
1 2 3 4 5 |
1: before 3: after post 5: after dispatch 2: post handler 4: dispatch handler |
handler 内部调用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
asio::io_context io; io.post([&io]{ std::cout << "A: outer handler start\n"; io.post([]{ std::cout << "B: nested post\n"; }); std::cout << "A: after nested post\n"; io.dispatch([]{ std::cout << "C: nested dispatch\n"; }); std::cout << "A: after nested dispatch\n"; }); io.run(); |
|
1 2 3 4 5 |
A: outer handler start A: after nested post C: nested dispatch A: after nested dispatch B: nested post |
什么时候必须用 post 而不是 dispatch
- 当你需要严格保证"顺序"或者"不被打乱"时,用
post- 因为
dispatch的内联执行会打乱你原本以为的执行顺序
- 因为
|
1 2 3 4 5 |
// 你希望这几个任务严格按顺序依次执行,不希望其中某个因为满足内联条件 // 而插队提前执行,就应该全用 post io.post(task1); io.post(task2); io.post(task3); |
如果想主动"让出"当前的调用栈,避免栈溢出
|
1 2 3 4 5 6 7 |
void loop(int n) { if (n <= 0) return; // 如果用 dispatch,n 很大时可能内联递归导致栈溢出 // 用 post,保证每次都从队列重新取出执行,调用栈不会累积 io.post([n]{ loop(n - 1); }); } |
strand 场景下的区别
strand(用来保证一组handler不会被并发执行,替代加锁)包装出来的executor,对dispatch的"是否内联"判断会更严格一些- 不仅要看"当前线程是否在这个
io_context的调用链上",还要看"当前是否已经在这个strand内部执行"(即没有其他handler正通过这个strand在跑) - 如果满足,同样可以内联
- 否则即便当前线程正在跑别的
io_context任务,也会乖乖排队,以保证strand"同一时刻只有一个handler在跑"这个核心承诺不被破坏
- 不仅要看"当前线程是否在这个
executor
概述
- 现代
ASIO里executor是"执行的目标/载体"这个抽象概念,post/dispatch提交任务时都是提交给某个executor,而不是绑死在io_context身上
任务队列
- 现代
ASIO(Executor模型)里,"任务队列"不一定专属于io_contextpost/dispatch严格来说不是io_context的专属操作,而是任意一个executor都要支持的通用操作(asio::post(executor, handler)、asio::dispatch(executor, handler),是自由函数,不只是成员函数
io_context- 是最常见的一种
executor,它维护了那个任务队列
- 是最常见的一种
thread_pool- 也是一种
executor,也有自己的任务队列
- 也是一种
strand包装出来的executor- 比如
asio::strand<io_context::executor_type> - 它自己并不维护一个独立的任务队列,而是包装、代理一个底层的
executor(通常就是某个io_context) - 任务提交给
strand时,strand内部会做一层"排他性调度"的逻辑(保证同一时刻只有一个任务在跑、保证提交顺序被遵守),但最终这些任务还是会被转交给它包装的那个底层executor的队列去真正排队、执行
可以理解成strand是一层"中间调度逻辑",不是"独立的队列容器"
- 比如
system_executor- 这是一个更轻量的
executor,通常直接用系统线程池风格的方式立刻派发任务,语义上更接近"没有排队,来了就跑" - 不像
io_context那样有个显式的、需要你调用run()才会被处理的队列
- 这是一个更轻量的
整条链路
|
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 |
你写: acceptor_.async_accept(lambda) ↓ async_initiate(...) — 包装 completion token ↓ initiate_async_move_accept::operator() — 校验 handler 签名 ↓ service.async_move_accept(...) ↓ ① op::ptr::allocate(handler) — 从 thread_info_base 拿内存 ② new (p.v) op(..., handler, ...) — placement new,lambda 存进 op 此时你的 lambda 正式"变成"了一个 win_iocp_socket_move_accept_op ↓ start_accept_op(...) ↓ ::AcceptEx(..., op) — op 指针交给内核,作为 OVERLAPPED* ↓ [异步等待,io.run() 阻塞在 GetQueuedCompletionStatus] ↓ [新连接到来,内核完成 AcceptEx] ↓ GetQueuedCompletionStatus 返回,取出 op 指针 ↓ op->complete(...) → 跳转到 func_ 指向的 do_complete ↓ do_complete 内部:转型回真实类型,构造出 tcp::socket 对象, 通过 fenced_block + 调用你的 lambda ↓ 你的 lambda 真正执行: std::cout << "new client\n"; ... |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++_ 引用折叠、万能引用、完美转发、auto推导、函数指针引用、顶层const、底层const04/30
- ♥ 密码保护:Reading 2025 记录02/12
- ♥ CMake教程一06/20
- ♥ STL_list05/04
- ♥ Windows 核心编程 _ 内核对象二06/07
- ♥ Reading 2020 《三闲集》10/26
热评文章
- ASIO:学习一 0