Lely CANopen ev_loop 详解

在这里插入图片描述

文章目录

1. 先区分三个容易混淆的“停止”状态

loop.c 中至少存在三类停止或完成状态:

状态 所属范围 作用
loop->stopped 每个 ev_loop_t 整个事件循环是否处于 stopped 状态;ev_loop_stop() 会影响所有正在运行该 loop 的线程。
ev_loop_thrd.stopped 每个线程 中断该线程当前最内层的一次 run/wait 调用;返回后通常会清零,以便下次重新运行。
ctx->ready 每个等待 context 表示正在等待的 future 已经 ready,应结束本次等待。

不能把它们当作同一个状态:

loop->stopped
= loop 级停止

thread-local stopped
= 线程级、单次运行中断

ctx->ready
= 本次等待目标已经完成

ev_loop_ctx_wait_one() 的主循环实际同时检查这些条件:

loop 没有全局停止
并且
当前线程没有被 kill
并且
future 尚未 ready

2. ntasks:outstanding work 计数

2.1 统计边界:不等于任务链表长度

源码注释把 ntasks 描述为 pending work 计数,其语义可以理解为:

事件循环已知但尚未完成的工作数量

公开接口文档进一步定义 outstanding work 为:

待执行任务
+ 当前执行中的任务
+ ev_exec_on_task_init() 调用次数
- ev_exec_on_task_fini() 调用次数

ev_exec_on_task_init() 的典型意义是:

我现在还没有把任务放入队列,但未来会提交工作,事件循环不要因为暂时空队列而退出。

ev_exec_on_task_fini() 表示:

先前声明的这份 outstanding work 已经结束,不再需要阻止事件循环退出。

因此,ntasks 的关键价值是覆盖“任务尚未入队,但异步操作仍然存活”的窗口。

2.2 队列为空不代表异步工作已经结束

假设一个异步 CAN 读取操作已经启动,但当前 SocketCAN 没有数据:

任务队列暂时为空
SocketCAN read 正等待 I/O
未来收到 CAN 帧后才会投递完成任务

如果 event loop 只看任务队列,就可能错误判断:

queue 为空 → 没有工作 → stop

ntasks > 0 表示仍存在已登记的异步工作,因此 loop 应继续等待 Poll 或条件变量事件。

2.3 ntasks 的状态变化

初始化为 0

on_task_init 0→1

on_task_init / on_task_fini 后仍大于 0

on_task_fini 1→0

加锁检查任务队列

队列仍有任务

队列为空,调用 ev_loop_do_stop

Zero

Active

LastDone

StopCheck

Stopped

关键代码路径是:

on_task_init:
    ntasks 原子加一

on_task_fini:
    ntasks 原子减一
    若减前为 1,即减后为 0:
        获取 loop->mtx
        若任务队列为空:停止 loop

3. ntasks 的线程安全与原子内存序

3.1 跨线程并发更新是原子化的根本原因

ev_exec_on_task_init()ev_exec_on_task_fini() 是 executor 的公开生命周期接口。调用它们的线程不一定是正在运行 ev_loop_wait_one() 的线程,也不保证已经持有 loop->mtx

典型并发场景:

线程 A:启动异步读,调用 on_task_init()
线程 B:启动异步写,调用 on_task_init()
线程 C:某个异步操作结束,调用 on_task_fini()
线程 D:event loop 读取 ntasks 判断是否应停止

如果使用普通 size_t 且没有统一互斥锁,会出现 C 语言层面的数据竞争,行为未定义。

3.2 非原子操作会出现丢失更新

假设 ntasks == 5,两个线程同时执行普通的 ++

线程 A 读取 5
线程 B 读取 5
线程 A 写入 6
线程 B 写入 6

正确结果应为 7,实际可能为 6。

对于 ntasks,计数错误会带来两类严重后果:

错误方向 后果
少计 loop 可能在异步操作尚未结束时提前 stop。
多计 loop 可能一直认为还有 outstanding work,无法自然退出。

3.3 未统一使用 loop->mtx 的原因

理论上可以把所有 ntasks 读写都放进 loop->mtx,但当前实现选择原子计数,原因可以从代码行为推断为:

  1. on_task_init() 是高频轻量路径,只做原子加一,不需要锁任务队列;
  2. on_task_fini() 在减后仍不为 0 时,也不需要进入互斥锁;
  3. 只有最后一个 outstanding work 消失时,才加锁检查队列并决定是否 stop;
  4. 避免每次工作引用变化都与任务队列竞争同一把锁。

这是典型的“原子引用计数快路径 + 最后一个引用进入慢路径”的结构。

3.4 加一 relaxed、减一 release 和归零 acquire fence

Linux/POSIX 原子实现中:

加一:relaxed
减一:release
观察到 1→0:acquire fence

这里最基本的要求是:

  • 所有线程必须对同一个计数进行不可分割的 read-modify-write;
  • 单个变量的修改顺序必须一致;
  • 最后一个完成者在执行“归零后的停止判断”前,需要建立更强的内存顺序边界。

relaxed 加一足以完成“占一个 outstanding work 名额”;它不负责发布其他数据。

减一使用 release,并在最后一个减一线程上执行 acquire fence,是常见的最后引用释放模式:只有真正从 1 变为 0 的线程承担最终收尾工作。

需要强调:

原子操作主要解决的是 ntasks 自身的并发计数问题;任务队列、waiting/polling 链表、loop->stopped 等结构仍由 loop->mtx 保护。

3.5 不同构建配置下的计数类型

源码针对不同平台和裁剪配置提供多个实现:

配置 实现
禁用线程 普通 size_t,因为不存在并发访问。
Windows MSVC InterlockedIncrement/Decrement
支持 C11 原子 atomic_size_t
明确禁用原子且特定配置 普通整数,依赖该裁剪配置的使用约束。

因此,“为什么要原子”准确地说是:

在启用多线程且平台支持原子操作的正常构建中,ntasks 必须以原子方式维护;单线程构建不需要。


4. 线程局部 ev_loop_thrd 与 context 栈

4.1 TLS 实例按线程独立

定义使用 _Thread_local

static + _Thread_local + struct ev_loop_thrd

三个关键词分别表示:

关键字 含义
static 变量名只在当前源文件可见。
_Thread_local 每个线程拥有独立存储实例。
静态初始化 {0, NULL} 每个线程第一次获得的实例初始为停止标志 0、context 指针 NULL。

这里的“每个线程一份”是 C TLS 的存储语义,不应理解为进程启动时预先为未来所有线程集中创建对象。对实际存在并访问该符号的线程,运行库提供该线程自己的实例,并以 { 0, NULL } 初始化。

因此,假设有三个线程调用同一个 ev_loop_t

线程 A:自己的 ev_loop_thrd
线程 B:自己的 ev_loop_thrd
线程 C:自己的 ev_loop_thrd

它们之间不会共享 stoppedctx 字段。

4.2 线程状态与 loop 状态相互独立

同一个线程可以先后运行不同的 loop,甚至在任务回调中嵌套运行另一个 loop。该线程仍只有一个 ev_loop_thrd

这也是为什么结构体名称是 ev_loop_thrd,而不是 ev_loop_thread_of_loop

4.3 两个字段的职责

stopped:中断当前线程正在执行的最内层 loop 调用
ctx:指向当前线程最内层的 ev_loop_ctx

ev_loop_self() 返回的实际上就是当前线程这份 TLS 状态的地址。这个地址被当成线程令牌传给 ev_loop_kill()

4.4 ctx 构成嵌套调用栈

创建 context 时:

新 ctx.next = 当前 TLS ctx
TLS ctx = 新 ctx

销毁时:

TLS ctx = 当前 ctx.next

形成的结构是:

TLS ev_loop_thrd.ctx
        ↓
最内层 ctx
        ↓ next
外一层 ctx
        ↓ next
更外层 ctx

线程局部 ev_loop_thrd

最内层 ctx

外层 ctx

最外层 ctx

NULL

所以 ev_loop_kill() 的公开契约能够实现:

存在嵌套 event loop 时,只中断最内层 loop。

它通过 thr->ctx 直接找到栈顶 context,而不是遍历某个全局 loop 列表。

4.5 ev_loop_ctx 不是 ev_loop_t

二者是“被管理对象”和“单次运行上下文”的关系:

ev_loop_ctx.loop ──指向──> ev_loop_t

一个 ev_loop_t 可以同时被多个线程运行,因此可以同时存在多个 context;同一线程发生嵌套 wait/run 时,也可以在 TLS 栈上挂载多个 context。每个 context 只记录本次调用需要的状态,例如:

  • 使用哪个 loop
  • 是否等待某个 future
  • 当前正在 Poll 还是条件变量等待;
  • Poll 后端线程令牌;
  • 所属线程的 pstopped
  • future 是否 ready。

4.6 context 是按需创建的

ev_loop_ctx_wait_one() 先尝试从任务队列取得真实任务。只有没有任务可立即执行,并且需要进入 Poll、条件变量等待或注册 future 完成通知时,才调用 ev_loop_ctx_create()。因此:

调用 wait_one/run
    ├─ 立即取到任务:可以不创建 context
    └─ 需要等待或 Poll:创建 context 并压入线程 TLS 栈

这也是 pctx 在第一次进入 ev_loop_ctx_wait_one() 时允许为 NULL 的原因。


5. pstopped:context 到线程中断标志的引用

5.1 指向当前线程 TLS 中的 stopped

context 创建时建立关系:

ctx->pstopped
      ↓
当前线程的 ev_loop_thrd.stopped

所以 pstopped 不是指向 loop->stopped,也不是 context 自己单独分配的停止标志。

5.2 context 保存该指针的用途

ev_loop_ctx 可能被以下执行路径访问:

  • 当前运行线程的 wait 主循环;
  • future 完成后执行的 ctx->task
  • 其他线程调用 ev_loop_kill()
  • loop stop 或新任务到来时的唤醒逻辑。

这些路径需要操作“该 context 所属线程的中断标志”。其他线程不能通过普通变量名取得目标线程的 TLS 实例,但 context 在创建时已经把目标 TLS 地址保存为 pstopped,因此可以定向访问。

5.3 线程级停止标志优于 context 私有标志

从现有设计可以看出,停止语义刻意绑定到线程,而不是单个 context 对象:

  1. ev_loop_self() 返回线程令牌;
  2. ev_loop_kill() 接收线程令牌;
  3. TLS 中维护最内层 context;
  4. 中断后标志会在本次 wait 返回前清零,使下一次调用能够继续运行。

如果每个 context 有独立停止字段,就需要额外处理:

  • 没有 context 时发生的 kill;
  • context 尚未创建但线程即将进入等待的窗口;
  • 嵌套 context 的停止传播规则;
  • ev_loop_self() 所代表的线程级语义。

当前方案用一个线程级标志统一这些状态。

5.4 pstopped 的同步保护

虽然指向的是线程局部对象,但该指针可能被其他线程使用。源码通过 loop->mtx 对相关访问进行同步:

  • wait 主循环在持有 loop->mtx 时读取;
  • ev_loop_kill() 持有 loop->mtx 时设置;
  • future 回调持有 loop->mtx 时检查;
  • wait 退出时持有 loop->mtx 清零。

因此,这个字段没有像 ntasks 一样采用原子类型。二者区别是:

ntasks:有无锁访问路径 → 必须原子化
pstopped 指向的值:统一在 loop->mtx 下访问 → 普通 int 即可

5.5 单次中断结束后的自动清零

线程级 stopped 的目标是中断“当前这一次”运行,而不是永久停止 loop。

若本次没有执行任务,并且检测到 thread-local stopped:

清零 stopped
返回调用者

因此下次再调用 run()wait_one() 时,可以重新进入事件循环。

永久或全局停止使用的是 loop->stopped,需要通过 ev_loop_restart() 重置。

5.6 loop 级停止与线程定向中断

两个公开接口的作用范围不同:

接口 作用范围 内部状态 恢复方式
ev_loop_stop(loop) 停止该 loop 上所有正在进行及后续的 run/wait 调用 设置 loop->stopped = 1,并唤醒 waiting/polling contexts ev_loop_restart(loop)
ev_loop_kill(loop, thr) 中断目标线程当前最内层的一次 run/wait 调用 通过栈顶 context 设置目标线程 TLS stopped,再 signal 条件变量或 kill Poll wait 本次调用返回前自动清零

所以不能表述为“需要整个线程停止时直接使用 stopped”。stopped 是内部标志;对外应使用 ev_loop_kill(loop, ev_loop_self() 返回的线程令牌)。该接口不会终止操作系统线程,只会让目标线程当前最内层的 event-loop 调用返回。


6. cnd:非 Poll 线程的休眠与唤醒

6.1 每个 context 独立持有条件变量

ev_loop_ctx 中包含:

cnd_t cond
waiting 标志

在 Linux pthread 兼容实现中:

cnd_t              → pthread_cond_t
cnd_wait()         → pthread_cond_wait()
cnd_timedwait()    → pthread_cond_timedwait()
cnd_signal()       → pthread_cond_signal()

它不是 CANopen 条件,也不是 Poll 的事件对象,而是纯线程同步原语。

6.2 进入条件变量等待的条件

只有同时满足以下条件时,线程才会进入 cnd_wait()

  1. 当前没有真实任务可执行;
  2. context 已经创建;
  3. 当前线程不能进入 ev_poll_wait()
  4. 任务队列仍为空;
  5. loop 尚未停止、线程未被 kill、future 未 ready。

“不能进入 Poll”通常是因为:

loop 配置了 npoll 限制
并且
已有足够数量的其他线程正在 ev_poll_wait()

POSIX reactor 场景通常建议 npoll == 1。这意味着:

一个线程负责 epoll_pwait
其他 loop 线程没有任务时用条件变量睡眠

6.3 Poll 并发数量限制

技术上 npoll == 0 可以允许无限数量线程同时 poll,但在 POSIX readiness reactor 模型中,多线程同时等待同一个事件源通常会增加:

  • 唤醒竞争;
  • 锁竞争;
  • 同一批 readiness 事件的调度复杂度;
  • 不必要的上下文切换。

Lely 的 C API 文档明确建议 POSIX reactor 使用单 polling thread。

因此条件变量承担“非 polling worker 的休眠机制”。

6.4 cnd_wait() 的原子解锁与重新加锁

调用前线程持有 loop->mtx。条件变量等待会完成一个原子组合动作:

释放 loop->mtx
并进入 cond 等待

被唤醒后:

重新获得 loop->mtx
然后 cnd_wait 返回

这里“释放锁并睡眠”必须是相对于 signal 过程原子的,否则会出现经典丢失唤醒:

等待线程:检查无任务
等待线程:准备睡眠但尚未真正睡眠
投递线程:加入任务并 signal
等待线程:此后才睡眠
结果:任务已存在,但等待线程永久睡眠

条件变量和互斥锁的组合就是为解决这个窗口。

6.5 条件变量的唤醒来源

ev_loop_ctx_kill() 根据 context 当前状态选择唤醒方式:

ctx 正在 cnd 等待
    → cnd_signal(&ctx->cond)

ctx 正在 Poll 等待
    → ev_poll_kill(loop->poll, ctx->thr)

ctx 两者都不是
    → 只更新状态,后续循环自行观察

触发 ev_loop_ctx_kill() 的主要来源包括:

  • 新任务从空队列变为非空;
  • future 变为 ready;
  • ev_loop_stop()
  • 其他线程调用 ev_loop_kill()

6.6 每个 context 独立条件变量的价值

它可以做到定向唤醒:

只唤醒被选中的 waiting context
而不是 broadcast 唤醒所有 worker

loop->waiting 链表按 LIFO 方式组织。ev_loop_kill_any() 取一个 waiting context,然后 signal 它自己的 cond

这减少了惊群效应。

6.7 虚假唤醒与循环复核

条件变量允许虚假唤醒,因此正确写法必须始终在循环中重新检查状态,而不能认为 cnd_wait() 返回就必然有任务。

Lely 的结构正是:

while 条件仍允许继续等待:
    检查 stop / killed / future
    检查任务队列
    检查 poll 资格
    必要时再次 wait

所以虚假唤醒不会破坏逻辑,只会多执行一轮条件检查。

6.8 条件变量与 Poll 的职责边界

等待方式 等待对象 唤醒机制
ev_poll_wait() 外部 I/O、timerfd、signal pipe 等 Poll 事件 ev_poll_kill() 或底层 I/O 事件
cnd_wait() loop 内部任务或状态变化 cnd_signal()

可以把它们理解为:

Poll:等外部世界
cnd:等同一进程内其他线程

7. Future/Promise:一次性异步结果与 event loop 的连接点

7.1 Future 不是消息队列

Lely 的 Future/Promise 可以宽泛地看成一种“异步完成通知机制”,但更准确的定义是:

Future/Promise 是围绕一份共享状态建立的一次性异步结果同步机制。

它与消息队列的区别如下:

机制 主要语义
消息队列 发送方可以连续写入多条消息,接收方逐条取出;通常是多次生产、多次消费。
Future/Promise Promise 最多成功完成一次;Future 观察同一个 ready 状态,并在 ready 后取得一次异步操作的结果。
Event-loop task queue 保存待执行的 ev_task,解决“在哪个 executor 上执行回调”;它不等同于 Future 的结果存储区。

因此,不宜把 Future 理解为“发送方把请求消息放进 loop 队列,接收方执行后再沿队列返回结果”。Future 自身不负责传输请求,也不负责执行 I/O;它只保存或关联异步操作的完成状态、结果值以及等待完成通知的任务。

7.2 Promise、Future、异步操作和 Loop 的角色

对象 准确角色
异步操作发起方 调用 AsyncRead()AsyncWrite()、异步 SDO 等接口,并取得 Future。
ev_promise_t 异步操作完成方持有的写端。操作成功、失败或取消后,通过 Promise 发布结果并将共享状态置为 ready。
ev_future_t 观察端。用于检查 ready、取得结果,或登记“Future ready 后应投递”的任务。
ev_loop_t 任务执行器和 I/O 驱动器。它执行 executor 队列中的任务,并在队列暂时为空时进入 Poll 或条件变量等待。
Poll/I/O 组件 驱动真正的 SocketCAN、timerfd、信号或其他异步事件;事件完成后通常投递任务,任务再推进异步操作。

ev_loop_t 不是固定的“发送方”或“接收方”。同一个 loop 既可能执行发起异步操作的任务,也可能执行 I/O 完成任务、Promise 完成后的通知任务以及其他应用任务。

7.3 Promise 与 Future 共享一次性结果状态

Promise 用于存入一个稍后由 Future 异步取得的值,其语义类似 single-shot event。Future 则提供对异步操作结果的访问。

典型状态变化为:

promise acquires completion right

store result and publish ready

later completion attempts are rejected

Waiting

Setting

Ready

Promise 完成后,Future 可以:

  1. 通过 ev_future_is_ready() 判断是否完成;
  2. 在 ready 后通过 ev_future_get() 取得 Promise 保存的结果;
  3. 通过 ev_future_submit() 登记一个任务,使其在 Future ready 后被投递到指定 executor。

这里的结果值与 executor task 是两个不同概念:结果保存在 Future/Promise 的共享状态中;task 只负责在完成时执行通知或后续处理。

7.4 ev_loop_wait(loop, future) 如何接入 Future

ev_loop_ctx_create() 收到非空 Future 时,它会:

1. 获取 Future 引用并保存到 ctx->future
2. 初始化 ctx->task,使其 executor 指向当前 loop
3. 调用 ev_future_submit(future, &ctx->task)

这一步不是把“异步请求”提交给 Future,而是把一个“Future 完成后唤醒本次 wait”的内部任务登记到 Future。

当异步操作完成并满足 Promise 后:

Promise 发布结果并置 Future ready
    ↓
Future 将 ctx->task 投递到 loop executor
    ↓
loop 从任务队列取出并执行 ctx->task
    ↓
ev_loop_ctx_task_func() 设置 ctx->ready = 1
    ↓
ev_loop_ctx_kill(ctx, 0) 唤醒 Poll 或条件变量等待
    ↓
ev_loop_wait() 看到 ctx->ready 后结束本次等待

ctx->task 的职责是结束等待,不是读取和返回业务结果。业务结果仍由调用方或上层包装在 Future ready 后通过 ev_future_get() 或相应的 C++ Future 接口取得。

7.5 等待 Future 时仍要持续驱动 Loop

ev_loop_wait(loop, future) 不是单纯阻塞在 Future 上。只要 Future 尚未 ready,它仍会继续推进 event loop:

Future 尚未 ready
    ├─ queue 有真实任务:执行一个任务
    ├─ 当前 context 可以 Poll:调用 ev_poll_wait() 等待 I/O
    └─ 当前 context 不可 Poll:调用 cnd_wait() 等待内部唤醒

这正是异步 CANopen 操作需要的行为。例如,异步 SDO 请求的完成依赖:

SocketCAN 收到响应或定时器到期
    ↓
Poll 回调投递 I/O 任务
    ↓
loop 执行任务并推进 SDO 状态机
    ↓
异步操作完成方满足 Promise
    ↓
Future ready,wait 返回

如果线程只是同步睡眠而不再驱动同一个 loop,且没有其他线程负责执行该 loop,那么依赖 CAN I/O 或超时任务的 Future 可能无法变为 ready。

7.6 更准确的完整流程

PromiseFuture SocketCAN PollBackend EventLoop AsyncOperation AsyncCaller PromiseFuture SocketCAN PollBackend EventLoop AsyncOperation AsyncCaller start async request return Future wait on Future submit internal wake task wait for I/O response or readiness event post completion task run completion task publish result and set ready post Future-ready task set context ready and wake wait wait returns obtain result

因此,更准确的理解是:

异步操作发起方启动请求并取得 Future;
Loop 持续执行任务和驱动 I/O;
异步操作完成方通过 Promise 发布结果;
Future 把完成通知任务投递到 Loop;
Loop 执行该通知任务并结束 wait;
调用方再从 Future 取得结果。

7.7 run()wait(future) 的区别

调用 内部 Future 主要返回条件
ev_loop_run(loop) NULL loop stopped,或 outstanding work 归零触发 stop。
ev_loop_wait(loop, future) 指定 Future Future ready、loop stopped、线程被中断或发生错误。
ev_loop_wait_until(loop, future, deadline) 指定 Future 在上述条件外增加绝对超时。
ev_loop_poll(loop) NULL 非阻塞处理当前可立即执行的工作。

Future 在 wait() 中是“本次 event-loop 运行的结束条件之一”,而不是 loop 队列中的普通业务任务。


8. 任务投递后的 Poll/cnd 唤醒路径

8.1 任务投递路径

任务被投递到 event loop 时:

获取 loop->mtx
判断投递前队列是否为空
把 task 放到 queue 尾部
若此前为空,调用 ev_loop_kill_any(loop, 1)
释放 loop->mtx

只在“空 → 非空”转换时唤醒线程,避免每次追加任务都重复发唤醒。

8.2 waiting context 的优先级

ev_loop_kill_any() 的顺序是:

先找 waiting 链表中的 context
若存在:通过 cnd_signal 唤醒
否则在允许时找 polling context
若存在:通过 ev_poll_kill 唤醒

原因很直接:

  • 条件变量线程本来就在等内部任务;
  • 唤醒它不需要打断正在处理外部 I/O 的 poller;
  • 可让 poller 继续服务 I/O readiness。

8.3 完整时序

Poll 等待线程 条件变量等待线程 ev_loop 投递线程 Poll 等待线程 条件变量等待线程 ev_loop 投递线程 alt [存在 waiting context] [没有 waiting,但存在 polling context] ev_exec_post(task) lock + queue push cnd_signal(ctx.cond) 重新取得 loop.mtx 从 queue 取任务 ev_poll_kill() 底层 Poll wait 被中断 返回并从 queue 取任务

9. ev_poll_wait() 的虚函数分派链

9.1 抽象接口分派

ev_poll_t 不是一个固定的 Linux io_poll 类型,而是一个抽象 Poll 接口。它的虚函数表包含:

self
wait
kill

ev_poll_wait() 做的事情等价于:

调用当前 poll 对象虚函数表中的 wait 指针

因此:

ev_poll_wait
不是具体后端函数
而是统一分派入口

9.2 Linux io::Poll 后端注册

Linux io_poll 初始化时:

poll_vptr = io_poll_poll_vtbl

该虚函数表的 wait 成员指向:

io_poll_poll_wait

io_poll_get_poll() 返回的不是新对象,而是 io_poll 内部 poll_vptr 字段的地址。ev_loop 保存这个抽象接口指针。

完整对象关系:

ev_loop.poll

io_poll.poll_vptr 的地址

io_poll_poll_vtbl

self = io_poll_poll_self

wait = io_poll_poll_wait

kill = io_poll_poll_kill

9.3 完整调用链

ev_loop_ctx_wait_one_until()
  ↓
ev_poll_wait(loop->poll, msec)
  ↓
(*loop->poll)->wait(loop->poll, msec)
  ↓
io_poll_poll_wait(..., msec)
  ↓
epoll_pwait(epfd, events, maxevents, msec, signal_mask)

因此,在该对象组合下可以确定:

如果 loop->poll 来自 Linux io_poll_get_poll(),执行的就是 io_poll_poll_wait()

但要加两个限定:

  1. 它是通过函数指针间接调用,不是 loop.cio_poll_poll_wait() 的直接静态调用;
  2. 换成 Windows Poll 或自定义 ev_poll_t 实现后,会调用对应后端的 wait

9.4 与 POSIX poll(2) 的区别

名称容易产生误导:

ev_poll_wait:Lely 抽象接口
io_poll_poll_wait:Linux io2 后端函数
epoll_pwait:实际 Linux 系统调用

它没有调用传统 POSIX poll()

9.5 msec 在两个 wait 函数中的含义

调用场景 msec
无超时 wait,队列为空 -1,无限阻塞
无超时 wait,但已有任务 0,只检查一次 I/O
until,有未来 deadline 且队列为空 deadline 剩余毫秒
until,deadline 为 NULL 0,完全非阻塞
until,队列已非空 0,避免阻塞任务执行

注意:ev_poll_wait() 的返回值表示 Poll 调用成功或失败,并不等于“执行了几个 loop task”。Poll 后端可能在内部处理多个 I/O watch 回调,这些回调通常再把任务投递到 loop 队列;loop 下一轮才执行对应 task。


10. 从 Loop::run() 到 SocketCAN 事件的完整流程

结合前一篇 io_poll 分析,可以把完整运行链路串起来:

1. C++ 调用 loop.run()
2. loop.hpp 转调 ev_loop_run()
3. ev_loop_run() 转调 ev_loop_wait(loop, NULL)
4. ev_loop_wait() 在外层循环中重复调用 ev_loop_ctx_wait_one()
5. 每次 wait_one 最多主动执行一个真实队列任务;若当前没有任务但 ntasks > 0,loop 不会 stop
6. 若当前线程有 Poll 资格,调用 ev_poll_wait(..., -1)
7. 虚函数分派到 io_poll_poll_wait()
8. io_poll_poll_wait() 在 epoll_pwait() 阻塞
9. SocketCAN fd 可读,epoll 返回
10. io_poll 找到对应 io_poll_watch 并调用 watch 回调
11. CanChannel watch 回调向 executor 投递接收任务
12. 投递使 loop queue 从空变为非空,并中断 Poll wait 或唤醒 cnd waiter
13. ev_loop_ctx_wait_one() 重新检查 queue
14. 弹出并执行 CanChannel 接收任务
15. 读取 CAN 帧,完成异步操作并进入 CanNet/CANopen 上层
ExecutorQueue SocketCAN EpollWait IoPoll PollVtable EventLoop Application ExecutorQueue SocketCAN EpollWait IoPoll PollVtable EventLoop Application call run queue empty and outstanding work exists wait with infinite timeout call backend wait enter epoll wait file descriptor becomes readable return input event callback posts receive task queue changes from empty to nonempty pop one task read CAN frame

11. npoll = 1 的典型多线程运行

假设:

npoll = 1
线程 A、B 都调用 loop.run()

可能出现:

线程 A:取得 Poll 资格,进入 epoll_pwait()
线程 B:无 Poll 资格,进入 cnd_wait()

此时另一个线程 C 投递一个普通任务:

线程 C:queue 空→非空
线程 C:优先 signal 线程 B 的 cond
线程 B:醒来并执行任务
线程 A:继续等待 I/O

如果没有 waiting 线程,才会中断 Poll 线程 A:

线程 C:ev_poll_kill(A 的 poll thread token)
线程 A:epoll_pwait 被信号打断
线程 A:返回 loop,执行新任务

这种设计让:

  • Poll 线程尽量持续负责外部 I/O;
  • 非 Poll worker 优先处理内部任务;
  • 必要时仍能中断 Poll,保证任务投递不会长期等待。

12. 关键对象关系

对象/字段 归属 是否共享 保护方式 主要职责
ev_loop_t 每个 loop 多线程共享 loop->mtx + 部分原子 任务队列、停止状态、poller/waiter 管理
loop->queue 每个 loop 多线程共享 loop->mtx 待执行任务队列
loop->ntasks 每个 loop 多线程共享 原子操作 outstanding work 计数
loop->stopped 每个 loop 多线程共享 loop->mtx loop 全局停止状态
ev_loop_thrd 每个线程 不在线程间共享实例 TLS;其字段跨线程访问时受 loop->mtx 保护 线程级中断和嵌套 context 栈顶
ev_loop_ctx 按需创建的一次 wait/run context 可被 future/kill 路径访问 loop->mtx + refcnt 连接 loop、线程、Poll/cond 和可选 future;不是 loop 对象
ctx->pstopped context 指向线程 TLS 指向目标线程实例 loop->mtx 检查或设置该线程的单次中断标志
ctx->cond 每个 context 目标线程等待、其他线程 signal 条件变量 + loop->mtx 非 Poll 线程休眠与唤醒
ctx->thr 每个 context 保存 Poll 后端线程令牌 后端定义 通过 ev_poll_kill() 中断具体 Poll wait
ctx->future 可选,每个 context 与 promise 共享异步完成状态 future mutex/atomic state + refcount future ready 时投递 ctx->task 并结束本次 wait
ev_promise_t 每个异步结果生产端 可跨线程共享 future mutex/atomic state + refcount 一次性写入结果并发布 ready
loop->poll 每个 loop 可共享后端 Poll ev_poll 接口契约 对外部事件进行抽象等待

13. 参考资料

13.1 Lely 官方源码

  1. Lely src/ev/loop.c
  2. Lely include/lely/ev/loop.h
  3. Lely include/lely/ev/loop.hpp
  4. Lely include/lely/ev/poll.h
  5. Lely Linux src/io2/linux/poll.c
  6. Lely pthread 兼容实现 src/libc/threads-pthread.c
  7. Lely executor API include/lely/ev/exec.h
  8. Lely standard executor src/ev/std_exec.c
  9. Lely Future API include/lely/ev/future.h
  10. Lely Future implementation src/ev/future.c
  11. Lely library overview

13.2 线程与语言规范

  1. The Open Group: pthread_cond_wait() / pthread_cond_timedwait()
  2. The Open Group: pthread_cond_signal()
  3. WG14 N1570: C11 draft

13.3 关联文章

本文的章节结构、结论先行、流程图、状态表和源码索引方式参考了已上传的《Lely canopen io_poll 机制原理与完整运行流程详解》。两篇文章可以连续阅读:

ev_loop:决定何时执行 task、何时等待、怎样协调多个线程
io_poll:决定怎样等待 Linux fd、怎样分发 epoll readiness
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐