今天遇到一个奇怪的问题,在与其他子系统交互的 Session 中,在发送某个 类型的 Message 之后,发送线程超时返回了,接收线程也没有收到任 何Message 。子系统的 Session 是这样实现的,我们的子系统 A 作为服务器端,上电后等待其他子系统连接上来 并创建对应的 Session B。Session 使用两个共享内存的缓冲区分别作为发送和接受的缓冲区, 接收线程使用使用中断的方式等待 Message 到来;由于技术上的限制,发送线程使用论询握手位 的方式等待对方的接收缓冲区转变为空闲才能往缓冲区写数据并发出中断。
交互的协议规定,发送一个 Message 之后必须等到对方回应才能接续发另外的 Message, 所以这其实是一种半双工的实现。这样,A 的发送线程 TSend 在发出一个带有序列号的 Message 之后, 发出中断。之后会隔某个时间去获取接收队列RL的锁并查找是否有对应序列号的 Message, 如果没有则在条件变量RLC上等待并放开RL锁。直到超时或B返回的带有相同序列号的 Message 才算完成一次交互。
为了防止 A 在等待回应 Message 的过程中错过B主动上报的 Message,A 的接受线程 TRecv 在收到 Message 后 获取 RL锁,并将 Message 放到一个队列中,通知RLC并释放RL锁。
但 是A 在收到某个类型的 Message 之后需要调用特殊的处理函数,这个特殊处理函数在做 很深的嵌套调用之后需要将一个 Message 发送给 B。问题就出现在这里,处理函数由 TRecv 来执行,但在发送 Message 给 B 时它自己又充当了 TSend 的角色,这 样TSend 在等待 B 返回对应 Message 时已经阻塞了,其实是TRecv阻塞住了,这样即使B收到TSend发出的 Message,返回 对应 的Message 给 A 并发送中断之后,A 已经没有线程来处理这个中断了,导致 TSend(TRecv)超时 返回之后才有线程处理中断。
这样实现的缺点
-
虽然 A 与其他子系统的交互不频繁,这样实现也不会有什么瓶颈,但是用接收线程 TRecv 做业务逻辑实在是不妥,它应该负责解包并向上提交就够了。
-
这里才是致命的,TRecv 不仅仅充当了解包的任务,在完成业务逻辑的时候不知 不觉的又扮演了 TSend 的角色,这很容易发生死锁,死锁在多线程环境中是除了名的难题。 在使用像 Windows IO Completion 这样的异步收发机制里,这个问题会更加的隐蔽。
这样实现比较好
TRecv 在收到 Message 之后将它发给消息队列,自己之负责接收 Message 并 送入消息队列。消息队列那边的事件循环负责消息的分发与执行业务逻辑。这样的职责划分能 带来很多好处。
在高并发的场景里,TRecv 负责调用 IOCP 或 epoll 的异步 API。消息队列之后也可以 放置负载均衡模块。这样 TRecv 在处理“短命”的连接时效率会高很多。