Lely CANopen ev_loop 详解
@[toc]
1. 先区分三个容易混淆的“停止”状态
loop.c 中至少存在三类停止或完成状态:
| 状态 | 所属范围 | 作用 |
|---|---|---|
loop->stopped |
每个 ev_loop_t |
整个事件循环是否处于 stopped 状态;ev_loop_stop() 会影响所有正在运行该 loop 的线程。 |
ev_loop_thrd.stopped |
每个线程 | 中断该线程当前最内层的一次 run/wait 调用;返回后通常会清零,以便下次重新运行。 |
ctx->ready |
每个等待 context | 表示正在等待的 future 已经 ready,应结束本次等待。 |
不能把它们当作同一个状态:
1 | loop->stopped |
ev_loop_ctx_wait_one() 的主循环实际同时检查这些条件:
1 | loop 没有全局停止 |
2. ntasks:outstanding work 计数
2.1 统计边界:不等于任务链表长度
源码注释把 ntasks 描述为 pending work 计数,其语义可以理解为:
1 | 事件循环已知但尚未完成的工作数量 |
公开接口文档进一步定义 outstanding work 为:
1 | 待执行任务 |
ev_exec_on_task_init() 的典型意义是:
我现在还没有把任务放入队列,但未来会提交工作,事件循环不要因为暂时空队列而退出。
ev_exec_on_task_fini() 表示:
先前声明的这份 outstanding work 已经结束,不再需要阻止事件循环退出。
因此,ntasks 的关键价值是覆盖“任务尚未入队,但异步操作仍然存活”的窗口。
2.2 队列为空不代表异步工作已经结束
假设一个异步 CAN 读取操作已经启动,但当前 SocketCAN 没有数据:
1 | 任务队列暂时为空 |
如果 event loop 只看任务队列,就可能错误判断:
1 | queue 为空 → 没有工作 → stop |
而 ntasks > 0 表示仍存在已登记的异步工作,因此 loop 应继续等待 Poll 或条件变量事件。
2.3 ntasks 的状态变化
1 | stateDiagram-v2 |
关键代码路径是:
1 | on_task_init: |
3. ntasks 的线程安全与原子内存序
3.1 跨线程并发更新是原子化的根本原因
ev_exec_on_task_init() 和 ev_exec_on_task_fini() 是 executor 的公开生命周期接口。调用它们的线程不一定是正在运行 ev_loop_wait_one() 的线程,也不保证已经持有 loop->mtx。
典型并发场景:
1 | 线程 A:启动异步读,调用 on_task_init() |
如果使用普通 size_t 且没有统一互斥锁,会出现 C 语言层面的数据竞争,行为未定义。
3.2 非原子操作会出现丢失更新
假设 ntasks == 5,两个线程同时执行普通的 ++:
1 | 线程 A 读取 5 |
正确结果应为 7,实际可能为 6。
对于 ntasks,计数错误会带来两类严重后果:
| 错误方向 | 后果 |
|---|---|
| 少计 | loop 可能在异步操作尚未结束时提前 stop。 |
| 多计 | loop 可能一直认为还有 outstanding work,无法自然退出。 |
3.3 未统一使用 loop->mtx 的原因
理论上可以把所有 ntasks 读写都放进 loop->mtx,但当前实现选择原子计数,原因可以从代码行为推断为:
on_task_init()是高频轻量路径,只做原子加一,不需要锁任务队列;on_task_fini()在减后仍不为 0 时,也不需要进入互斥锁;- 只有最后一个 outstanding work 消失时,才加锁检查队列并决定是否 stop;
- 避免每次工作引用变化都与任务队列竞争同一把锁。
这是典型的“原子引用计数快路径 + 最后一个引用进入慢路径”的结构。
3.4 加一 relaxed、减一 release 和归零 acquire fence
Linux/POSIX 原子实现中:
1 | 加一:relaxed |
这里最基本的要求是:
- 所有线程必须对同一个计数进行不可分割的 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:
1 | static + _Thread_local + struct ev_loop_thrd |
三个关键词分别表示:
| 关键字 | 含义 |
|---|---|
static |
变量名只在当前源文件可见。 |
_Thread_local |
每个线程拥有独立存储实例。 |
静态初始化 {0, NULL} |
每个线程第一次获得的实例初始为停止标志 0、context 指针 NULL。 |
这里的“每个线程一份”是 C TLS 的存储语义,不应理解为进程启动时预先为未来所有线程集中创建对象。对实际存在并访问该符号的线程,运行库提供该线程自己的实例,并以 { 0, NULL } 初始化。
因此,假设有三个线程调用同一个 ev_loop_t:
1 | 线程 A:自己的 ev_loop_thrd |
它们之间不会共享 stopped 和 ctx 字段。
4.2 线程状态与 loop 状态相互独立
同一个线程可以先后运行不同的 loop,甚至在任务回调中嵌套运行另一个 loop。该线程仍只有一个 ev_loop_thrd。
这也是为什么结构体名称是 ev_loop_thrd,而不是 ev_loop_thread_of_loop。
4.3 两个字段的职责
1 | stopped:中断当前线程正在执行的最内层 loop 调用 |
ev_loop_self() 返回的实际上就是当前线程这份 TLS 状态的地址。这个地址被当成线程令牌传给 ev_loop_kill()。
4.4 ctx 构成嵌套调用栈
创建 context 时:
1 | 新 ctx.next = 当前 TLS ctx |
销毁时:
1 | TLS ctx = 当前 ctx.next |
形成的结构是:
1 | TLS ev_loop_thrd.ctx |
1 | flowchart TD |
所以 ev_loop_kill() 的公开契约能够实现:
1 | 存在嵌套 event loop 时,只中断最内层 loop。 |
它通过 thr->ctx 直接找到栈顶 context,而不是遍历某个全局 loop 列表。
4.5 ev_loop_ctx 不是 ev_loop_t
二者是“被管理对象”和“单次运行上下文”的关系:
1 | 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()。因此:
1 | 调用 wait_one/run |
这也是 pctx 在第一次进入 ev_loop_ctx_wait_one() 时允许为 NULL 的原因。
5. pstopped:context 到线程中断标志的引用
5.1 指向当前线程 TLS 中的 stopped
context 创建时建立关系:
1 | ctx->pstopped |
所以 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 对象:
ev_loop_self()返回线程令牌;ev_loop_kill()接收线程令牌;- TLS 中维护最内层 context;
- 中断后标志会在本次 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 一样采用原子类型。二者区别是:
1 | ntasks:有无锁访问路径 → 必须原子化 |
5.5 单次中断结束后的自动清零
线程级 stopped 的目标是中断“当前这一次”运行,而不是永久停止 loop。
若本次没有执行任务,并且检测到 thread-local stopped:
1 | 清零 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 中包含:
1 | cnd_t cond |
在 Linux pthread 兼容实现中:
1 | cnd_t → pthread_cond_t |
它不是 CANopen 条件,也不是 Poll 的事件对象,而是纯线程同步原语。
6.2 进入条件变量等待的条件
只有同时满足以下条件时,线程才会进入 cnd_wait():
- 当前没有真实任务可执行;
- context 已经创建;
- 当前线程不能进入
ev_poll_wait(); - 任务队列仍为空;
- loop 尚未停止、线程未被 kill、future 未 ready。
“不能进入 Poll”通常是因为:
1 | loop 配置了 npoll 限制 |
POSIX reactor 场景通常建议 npoll == 1。这意味着:
1 | 一个线程负责 epoll_pwait |
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。条件变量等待会完成一个原子组合动作:
1 | 释放 loop->mtx |
被唤醒后:
1 | 重新获得 loop->mtx |
这里“释放锁并睡眠”必须是相对于 signal 过程原子的,否则会出现经典丢失唤醒:
1 | 等待线程:检查无任务 |
条件变量和互斥锁的组合就是为解决这个窗口。
6.5 条件变量的唤醒来源
ev_loop_ctx_kill() 根据 context 当前状态选择唤醒方式:
1 | ctx 正在 cnd 等待 |
触发 ev_loop_ctx_kill() 的主要来源包括:
- 新任务从空队列变为非空;
- future 变为 ready;
ev_loop_stop();- 其他线程调用
ev_loop_kill()。
6.6 每个 context 独立条件变量的价值
它可以做到定向唤醒:
1 | 只唤醒被选中的 waiting context |
loop->waiting 链表按 LIFO 方式组织。ev_loop_kill_any() 取一个 waiting context,然后 signal 它自己的 cond。
这减少了惊群效应。
6.7 虚假唤醒与循环复核
条件变量允许虚假唤醒,因此正确写法必须始终在循环中重新检查状态,而不能认为 cnd_wait() 返回就必然有任务。
Lely 的结构正是:
1 | while 条件仍允许继续等待: |
所以虚假唤醒不会破坏逻辑,只会多执行一轮条件检查。
6.8 条件变量与 Poll 的职责边界
| 等待方式 | 等待对象 | 唤醒机制 |
|---|---|---|
ev_poll_wait() |
外部 I/O、timerfd、signal pipe 等 Poll 事件 | ev_poll_kill() 或底层 I/O 事件 |
cnd_wait() |
loop 内部任务或状态变化 | cnd_signal() |
可以把它们理解为:
1 | Poll:等外部世界 |
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 则提供对异步操作结果的访问。
典型状态变化为:
1 | stateDiagram-v2 |
Promise 完成后,Future 可以:
- 通过
ev_future_is_ready()判断是否完成; - 在 ready 后通过
ev_future_get()取得 Promise 保存的结果; - 通过
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 | 1. 获取 Future 引用并保存到 ctx->future |
这一步不是把“异步请求”提交给 Future,而是把一个“Future 完成后唤醒本次 wait”的内部任务登记到 Future。
当异步操作完成并满足 Promise 后:
1 | Promise 发布结果并置 Future ready |
ctx->task 的职责是结束等待,不是读取和返回业务结果。业务结果仍由调用方或上层包装在 Future ready 后通过 ev_future_get() 或相应的 C++ Future 接口取得。
7.5 等待 Future 时仍要持续驱动 Loop
ev_loop_wait(loop, future) 不是单纯阻塞在 Future 上。只要 Future 尚未 ready,它仍会继续推进 event loop:
1 | Future 尚未 ready |
这正是异步 CANopen 操作需要的行为。例如,异步 SDO 请求的完成依赖:
1 | SocketCAN 收到响应或定时器到期 |
如果线程只是同步睡眠而不再驱动同一个 loop,且没有其他线程负责执行该 loop,那么依赖 CAN I/O 或超时任务的 Future 可能无法变为 ready。
7.6 更准确的完整流程
1 | sequenceDiagram |
因此,更准确的理解是:
1 | 异步操作发起方启动请求并取得 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 时:
1 | 获取 loop->mtx |
只在“空 → 非空”转换时唤醒线程,避免每次追加任务都重复发唤醒。
8.2 waiting context 的优先级
ev_loop_kill_any() 的顺序是:
1 | 先找 waiting 链表中的 context |
原因很直接:
- 条件变量线程本来就在等内部任务;
- 唤醒它不需要打断正在处理外部 I/O 的 poller;
- 可让 poller 继续服务 I/O readiness。
8.3 完整时序
1 | sequenceDiagram |
9. ev_poll_wait() 的虚函数分派链
9.1 抽象接口分派
ev_poll_t 不是一个固定的 Linux io_poll 类型,而是一个抽象 Poll 接口。它的虚函数表包含:
1 | self |
ev_poll_wait() 做的事情等价于:
1 | 调用当前 poll 对象虚函数表中的 wait 指针 |
因此:
1 | ev_poll_wait |
9.2 Linux io::Poll 后端注册
Linux io_poll 初始化时:
1 | poll_vptr = io_poll_poll_vtbl |
该虚函数表的 wait 成员指向:
1 | io_poll_poll_wait |
io_poll_get_poll() 返回的不是新对象,而是 io_poll 内部 poll_vptr 字段的地址。ev_loop 保存这个抽象接口指针。
完整对象关系:
1 | flowchart LR |
9.3 完整调用链
1 | ev_loop_ctx_wait_one_until() |
因此,在该对象组合下可以确定:
如果
loop->poll来自 Linuxio_poll_get_poll(),执行的就是io_poll_poll_wait()。
但要加两个限定:
- 它是通过函数指针间接调用,不是
loop.c对io_poll_poll_wait()的直接静态调用; - 换成 Windows Poll 或自定义
ev_poll_t实现后,会调用对应后端的wait。
9.4 与 POSIX poll(2) 的区别
名称容易产生误导:
1 | ev_poll_wait:Lely 抽象接口 |
它没有调用传统 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 | 1. C++ 调用 loop.run() |
1 | sequenceDiagram |
11. npoll = 1 的典型多线程运行
假设:
1 | npoll = 1 |
可能出现:
1 | 线程 A:取得 Poll 资格,进入 epoll_pwait() |
此时另一个线程 C 投递一个普通任务:
1 | 线程 C:queue 空→非空 |
如果没有 waiting 线程,才会中断 Poll 线程 A:
1 | 线程 C:ev_poll_kill(A 的 poll thread token) |
这种设计让:
- 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 官方源码
- Lely
src/ev/loop.c - Lely
include/lely/ev/loop.h - Lely
include/lely/ev/loop.hpp - Lely
include/lely/ev/poll.h - Lely Linux
src/io2/linux/poll.c - Lely pthread 兼容实现
src/libc/threads-pthread.c - Lely executor API
include/lely/ev/exec.h - Lely standard executor
src/ev/std_exec.c - Lely Future API
include/lely/ev/future.h - Lely Future implementation
src/ev/future.c - Lely library overview
13.2 线程与语言规范
- The Open Group:
pthread_cond_wait()/pthread_cond_timedwait() - The Open Group:
pthread_cond_signal() - WG14 N1570: C11 draft
13.3 关联文章
本文的章节结构、结论先行、流程图、状态表和源码索引方式参考了已上传的《Lely canopen io_poll 机制原理与完整运行流程详解》。两篇文章可以连续阅读:
1 | ev_loop:决定何时执行 task、何时等待、怎样协调多个线程 |









