You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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);
        }
    }
}

关键改进点:

  1. 避免线程阻塞:await tcs.Task不会挂起当前线程,WebSocket的消息回调可以正常执行,从根源解决死锁。
  2. 高效查找:用Dictionary代替List,请求匹配效率从O(n)提升到O(1)。
  3. 线程安全:所有操作待处理请求集合的地方都加了lock,避免多线程并发操作导致的异常。
  4. 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);
        }
    });
}

额外注意事项

  1. 超时处理:可以给await加上超时,比如await tcs.Task.WaitAsync(TimeSpan.FromSeconds(10)),避免无限等待。
  2. 异常处理:在WebSocket断开、服务器返回错误时,要主动给TaskCompletionSource设置异常,避免客户端一直等待。
  3. 强类型代替dynamic:建议定义具体的请求/响应类,代替dynamic,提升代码安全性和可维护性。

内容的提问来源于stack exchange,提问作者user4125721

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 20:43:12