Skip to content

第 3 章:Session —— 把并发系统收进一条事件线程

tr_session 是 Transmission 最重要的运行时边界。本章拆解它如何拥有子系统、如何把跨线程调用排进 libevent,以及为什么“一个核心事件线程”并不等于“整个程序只有一个线程”。

一、Session 的两重身份

tr_session 同时是:

  • 所有权根:控制 Announcer、DHT、LPD、Peer Manager、RPC、文件池、验证器和定时器的生命周期;
  • 并发边界:绝大多数网络与 torrent 状态转换被串行化到专属 Session 线程。

这两个身份必须放在一起理解。一个子系统由 Session 持有,通常也意味着它的回调最终要回到 Session 线程。

二、Session 线程如何启动

tr_session_thread_impl 构造时创建 std::thread,线程内运行一个 libevent event_base。构造函数会等待事件循环真正开始,避免 Session 后续初始化把工作投递到一个尚未就绪的线程。

EVLOOP_NO_EXIT_ON_EMPTY 很关键:即使暂时没有 socket 或 timer 事件,主循环也不退出。Session 生命周期结束时才调用 event_base_loopexit()

三、run()queue():同一套 API,两种语义

tr_session_thread 暴露两个核心操作:

cpp
void queue(callback_t&& func);
void run(callback_t&& func);
  • queue() 永远把任务放进受 mutex 保护的工作队列,然后用 event_active() 唤醒事件循环;
  • run() 先检查 am_in_session_thread():已经在 Session 线程就立即执行,否则退化为 queue()

这种“内联或排队”语义避免了内部调用无意义地多绕一圈,同时让公共 API 可以安全地从 UI/RPC 线程调用。

工作队列的消费也有一个工程细节:回调开始时先把共享队列整体 swap 到局部变量,再释放锁并逐个执行。这样执行用户工作时不持有队列锁,新任务仍可并发入队。

text
其他线程:lock → push callback → unlock → event_active
Session线程:lock → swap whole queue → unlock → execute callbacks

四、为什么不为每个 Torrent 开线程

BitTorrent 任务很多,但单次状态变更通常很小:解析一个消息、更新 bitfield、安排几个请求、调整计时器。如果每个 Torrent/Peer 都有线程,会引入大量锁顺序、上下文切换和关闭竞态。

Session 线程模型的收益是:

  1. 网络事件天然适合 libevent;
  2. Torrent、Swarm、Peer Manager 的大部分可变状态可以按事件顺序处理;
  3. 定时任务与 socket 回调共享同一时间轴;
  4. UI 只需投递命令,不必进入协议对象内部加锁。

代价也明确:任何在 Session 线程里执行的慢 CPU/磁盘任务都会拖住所有 torrent。因此完整校验被移到独立 tr_verify_worker;HTTP 传输交给 libcurl 驱动;前端渲染也有自己的事件循环。

五、程序里实际有哪些并发域

“单事件线程”只是核心状态的主序列,不是把所有工作都同步塞进去。

六、Session 构造函数透露的依赖图

tr_session 的成员声明中有大量 depends-on 注释,这是阅读所有权最有价值的地方。例如:

  • timer_maker_ 依赖 session_thread_ 的 event base;
  • top_bandwidth_ 是 torrent/group/peer 带宽树的根;
  • web_ 通过 WebMediator 访问 Session 配置并把完成回调送回 Session 线程;
  • peer_mgr_ 依赖 timer、blocklist、torrents、web 与顶层带宽;
  • announcer_ 依赖 settings、torrents、web 与 UDP announcer;
  • rpc_server_ 依赖 session thread、timer、settings、torrents 与 web assets。

构造函数本身只显式初始化基础目录、线程、计时器、设置、Peer Manager、RPC server 和三个周期 timer;其余成员大量使用就地初始化。声明顺序就是构造顺序,逆序就是析构顺序,所以这些注释不是装饰,而是在保护 C++ 对象生命周期。

七、三个周期性“心跳”

Session 至少直接持有三个重复定时器:

定时器周期主要责任
now_timer_1 秒更新全局时间与周期性状态
queue_timer_1 秒检查下载/做种队列是否可启动新任务
save_timer_360 秒周期保存脏 torrent 的 resume 状态

其他子系统也在相同 TimerMaker 上建立自己的节拍:Announcer upkeep、DHT announce/bootstrap/periodic、Peer Manager rechoke/bandwidth/reconnect、LPD announce 等。

统一 timer 工厂有两个好处:时间回调都落在 Session 线程;测试可以通过 Mediator 替换或控制计时能力。

八、初始化不是一个构造函数就结束

外部入口 tr_sessionInit(config_dir, queueing, settings) 最终创建 Session,但完整启动还包括:

  1. 解析、修正并应用 settings;
  2. 初始化监听 TCP/UDP socket 与端口映射;
  3. 创建 DHT/LPD 等条件性子系统;
  4. 启动 RPC server;
  5. 由产品入口调用 tr_sessionLoadTorrents() 恢复任务;
  6. 每个 torrent 再根据 resume 的 run 状态决定排队、校验或启动。

tr_sessionLoadTorrents() 展示了同步 API 如何跨线程:它创建 promise/future,把加载任务投到 Session 线程,然后等待结果。这对启动阶段很方便,但也说明调用者绝不能在 Session 线程里再次以同样方式阻塞等待自己。

九、关闭为什么有两段 event loop

tr_session_thread_impl 析构时:

  1. 设置 is_shutting_down_,让 steady-state loop 退出;
  2. 再运行一次普通 event_base_loop(evbase, 0)
  3. 第二段循环在事件耗尽时退出,给已排队的收尾任务机会;
  4. 等待状态翻转并 join 线程。

这比“设置 flag 后立即 join”更安全:Tracker STOP、文件关闭、resume 保存和异步回调可能已经排队,粗暴终止会丢掉状态或造成悬空引用。

十、Mediator:避免 Session 变成全局变量

Session 里为多个子系统定义小型 Mediator:DhtMediatorLpdMediatorWebMediatorPortForwardingMediatorQueueMediator 等。子系统只依赖自己所需的方法,例如 DHT 只需要:

  • 可参与 DHT 的 torrent 列表;
  • 根据 id 取 info hash;
  • 配置目录与 TimerMaker;
  • 把发现的 Peer 加回系统。

这比把 tr_session* 直接传给所有模块更克制。它明确了依赖、方便测试替身,也防止子系统随手访问不相关状态。代价是 Mediator 类数量多,调用链多一层。

十一、线程安全阅读清单

读一个修改核心状态的函数时,检查:

  • 是否有 TR_ASSERT(session->am_in_session_thread())
  • 外部入口是否调用 run_in_session_thread()
  • 捕获裸 tor* 的异步 lambda 是否同时捕获 torrent id,并在执行时重新确认对象仍存在;
  • 回调是否可能在 Session 关闭后到达;
  • 是否持有 torrent/session mutex,又在回调里触发可能重入的逻辑。

Transmission 并没有完全依赖单线程假设;tr_torrent 仍有锁,Session 也暴露 unique_lock()。事件线程减少了并发面,但不能消除来自 UI、验证线程和异步完成回调的交叉。

源码锚点

文档采用 CC BY-SA 4.0;源码片段保留 Transmission 上游许可。