使用 SemaphoreSlim 解决竞争条件

Solving race condition using SemaphoreSlim

A race condition occurs when two threads access a shared variable at the same time. The first thread reads the variable, and the second thread reads the same value from the variable.

我在 StartAsync 中使用 SemaphoreSlim 来防止竞争条件,即两个线程可能同时更改 _clientWebSocket。如果我们在 Timer.

中调用 StartAsync,则可能会发生这种情况

StopAsync呢?如果 StopAsync 没有包装在 SemaphoreSlim 中,那么两个线程通过 if 条件并调用 CloseOutputAsync 两次的机会是多少?第一个将成功关闭输出,但第二个调用将失败并返回 WebSocketException,因为 Web 套接字已关闭。考虑到我们没有改变任何变量,这是否也是一种竞争条件?考虑到我的代码的其他部分没有关闭网络套接字,它是否会发生,我真的需要包装 CloseOutputAsync 的 try/catch 吗?

我在 StopAsync 中重用了 SemaphoreSlim 以防止同时调用 StartAsync 和 StopAsync,这是个好主意吗?

private readonly SemaphoreSlim _semaphore = new(1, 1);

public async Task StartAsync(string url)
{
    await _semaphore.WaitAsync().ConfigureAwait(false);

    try
    {
        var ws = new ClientWebSocket();
        _clientWebSocket = ws;

        await _clientWebSocket.ConnectAsync(new Uri(url), CancellationToken.None).ConfigureAwait(false); // TODO: Handle

        _tokenSource = new CancellationTokenSource();

        _ = ReceiveLoopAsync(ws, _tokenSource.Token);
        _ = SendLoopAsync(ws);
    }
    finally
    {
        _semaphore.Release();
    }
}

public async Task StopAsync()
{
    await _semaphore.WaitAsync().ConfigureAwait(false);

    try
    {
        if (_clientWebSocket is { State: not (WebSocketState.Aborted or WebSocketState.Closed or WebSocketState.CloseSent) })
        {
            try
            {
                await _clientWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false);
            }
            catch (Exception) // thrown when we try to close an already closed socket
            {
            }
        }

        _clientWebSocket?.Dispose();
        _clientWebSocket = null;

        _tokenSource?.Cancel();
    }
    finally
    {
        _semaphore.Release();
    }
}

我想说的是,如果您希望 StopAsync 被并行调用,那么这些措施是值得的。

尽管我在您的代码中发现了另一个潜在缺陷 - 您在开始和停止之间共享 _clientWebSocket_tokenSource,因此您可能会在下一种情况下结束:

  1. 线程 1 调用 StartAsync 并初始化 _clientWebSocket_tokenSource 并存在信号量
  2. 线程 2 调用 StartAsync 并覆盖 _clientWebSocket_tokenSource
  3. 线程 1 StopAsync 调用 StopAsync 将停止由 线程 2 创建的 ClientWebSocket但是没有人会关闭由 线程 1
  4. 创建的那个