如何用WebSocketSharp在C#项目中实现无死锁的类同步WebSocket请求?
刚好碰到过类似的问题,你的死锁问题根源在于阻塞了WebSocket消息处理的线程上下文——当你调用sr.resetEvent.WaitOne()时,当前线程被挂起,而WebSocketSharp的消息回调(OnReceivedMessage)刚好需要这个线程来执行,导致永远没法触发Set(),自然就死锁了。
下面给你两种解决方案,优先推荐第一种异步标准方案,既避免阻塞又保持“类同步”的编程体验:
最优方案:用TaskCompletionSource实现异步等待(推荐)
.NET里处理这种“发送请求等待响应”的场景,TaskCompletionSource<T>是标准工具,结合await可以完全避免线程阻塞,同时让代码逻辑清晰。
改进后的代码实现
// 替换原来的SynchronousRequest,用TaskCompletionSource实现异步等待 public class RequestContext<T> { public long RequestId { get; } public TaskCompletionSource<T> CompletionSource { get; } = new TaskCompletionSource<T>(); public RequestContext() { // 用NextInt64避免短时间创建多个实例导致ID重复 RequestId = new Random().NextInt64(); } } public class APIWebSocket : BaseAPIWebSocket { // 用字典存待处理请求,O(1)查找效率更高,且线程安全 private readonly Dictionary<long, TaskCompletionSource<dynamic>> _pendingRequests = new Dictionary<long, TaskCompletionSource<dynamic>>(); private readonly WebSocket _ws; // 单独实例化Random,避免短时间生成重复ID private readonly Random _requestIdGenerator = new Random(); public APIWebSocket() { _ws = new WebSocket("wss://www.someserver.com"); RegisterConnectionEvents(); } // 改成异步方法,调用方用await即可实现"类同步"逻辑 public async Task<dynamic> SendTickerRequestAsync() { var requestId = _requestIdGenerator.NextInt64(); var tcs = new TaskCompletionSource<dynamic>(); // 线程安全地添加到待处理列表 lock (_pendingRequests) { _pendingRequests.Add(requestId, tcs); } // 构造请求消息 var msg = new { jsonrpc = "2.0", method = "public/ticker", id = requestId, @params = new { instrument_name = "ETH" } }; _ws.Send(JsonConvert.SerializeObject(msg)); try { // 异步等待响应,不会阻塞当前线程 var response = await tcs.Task; return response; } finally { // 无论成功失败都清理待处理请求 lock (_pendingRequests) { _pendingRequests.Remove(requestId); } } } protected override void OnReceivedMessage(object sender, WebSocketSharp.MessageEventArgs e) { dynamic message = JsonConvert.DeserializeObject(e.Data); if (message.id == null) return; var requestId = (long)message.id; TaskCompletionSource<dynamic> tcs = null; // 线程安全地获取并移除待处理请求 lock (_pendingRequests) { if (_pendingRequests.TryGetValue(requestId, out tcs)) { _pendingRequests.Remove(requestId); } } // 触发await,返回响应 if (tcs != null) { tcs.SetResult(message); } } }
关键改进点:
- 避免线程阻塞:
await tcs.Task不会挂起当前线程,WebSocket的消息回调可以正常执行,从根源解决死锁。 - 高效查找:用
Dictionary代替List,请求匹配效率从O(n)提升到O(1)。 - 线程安全:所有操作待处理请求集合的地方都加了
lock,避免多线程并发操作导致的异常。 - ID唯一性:单独实例化
Random并使用NextInt64,避免短时间生成重复请求ID。
备选方案:在后台线程调用WaitOne()(不推荐)
如果坚持要用ManualResetEvent,可以把等待逻辑放到后台线程,避免阻塞WebSocket消息处理线程,但这种方案复杂度更高,容易引入其他线程问题:
public void SendSyncTest() { var sr = new SynchronousRequest(); // 线程安全添加请求 lock (waitingSyncRequests) { waitingSyncRequests.Add(sr); } var msg = new { jsonrpc = "2.0", method = "public/ticker", id = sr.requestId, @params = new { instrument_name = "ETH" } }; ws.Send(JsonConvert.SerializeObject(msg)); // 在后台线程等待,不阻塞当前线程 Task.Run(() => { sr.resetEvent.WaitOne(); // 处理响应(如果是UI应用,这里需要切换回UI线程) // do some processing on response here... // 线程安全移除请求 lock (waitingSyncRequests) { waitingSyncRequests.Remove(sr); } }); }
额外注意事项
- 超时处理:可以给
await加上超时,比如await tcs.Task.WaitAsync(TimeSpan.FromSeconds(10)),避免无限等待。 - 异常处理:在WebSocket断开、服务器返回错误时,要主动给
TaskCompletionSource设置异常,避免客户端一直等待。 - 强类型代替dynamic:建议定义具体的请求/响应类,代替
dynamic,提升代码安全性和可维护性。
内容的提问来源于stack exchange,提问作者user4125721
相关产品推荐
相关产品推荐

