第 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 暴露两个核心操作:
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 到局部变量,再释放锁并逐个执行。这样执行用户工作时不持有队列锁,新任务仍可并发入队。
其他线程:lock → push callback → unlock → event_active
Session线程:lock → swap whole queue → unlock → execute callbacks四、为什么不为每个 Torrent 开线程
BitTorrent 任务很多,但单次状态变更通常很小:解析一个消息、更新 bitfield、安排几个请求、调整计时器。如果每个 Torrent/Peer 都有线程,会引入大量锁顺序、上下文切换和关闭竞态。
Session 线程模型的收益是:
- 网络事件天然适合 libevent;
- Torrent、Swarm、Peer Manager 的大部分可变状态可以按事件顺序处理;
- 定时任务与 socket 回调共享同一时间轴;
- 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,但完整启动还包括:
- 解析、修正并应用 settings;
- 初始化监听 TCP/UDP socket 与端口映射;
- 创建 DHT/LPD 等条件性子系统;
- 启动 RPC server;
- 由产品入口调用
tr_sessionLoadTorrents()恢复任务; - 每个 torrent 再根据 resume 的 run 状态决定排队、校验或启动。
tr_sessionLoadTorrents() 展示了同步 API 如何跨线程:它创建 promise/future,把加载任务投到 Session 线程,然后等待结果。这对启动阶段很方便,但也说明调用者绝不能在 Session 线程里再次以同样方式阻塞等待自己。
九、关闭为什么有两段 event loop
tr_session_thread_impl 析构时:
- 设置
is_shutting_down_,让 steady-state loop 退出; - 再运行一次普通
event_base_loop(evbase, 0); - 第二段循环在事件耗尽时退出,给已排队的收尾任务机会;
- 等待状态翻转并 join 线程。
这比“设置 flag 后立即 join”更安全:Tracker STOP、文件关闭、resume 保存和异步回调可能已经排队,粗暴终止会丢掉状态或造成悬空引用。
十、Mediator:避免 Session 变成全局变量
Session 里为多个子系统定义小型 Mediator:DhtMediator、LpdMediator、WebMediator、PortForwardingMediator、QueueMediator 等。子系统只依赖自己所需的方法,例如 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、验证线程和异步完成回调的交叉。
源码锚点
libtransmission/session-thread.h、session-thread.cc:工作队列与双阶段关闭libtransmission/session.h:成员所有权、Mediator 和依赖注释libtransmission/session.cc:初始化、设置应用、定时器与 torrent 恢复libtransmission/timer-ev.cc:libevent 定时器适配libtransmission/verify.cc:独立验证线程