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.
I used SemaphoreSlim
in StartAsync
to prevent a race condition, i.e. two threads may change _clientWebSocket
concurrently. It may happen if we're calling StartAsync in a Timer
.
What about StopAsync
? If StopAsync wasn't wrapped in a SemaphoreSlim, what are the chances two threads to pass through the if
condition and call CloseOutputAsync twice? The first one would close the output successfully, but the second call would fail with WebSocketException because the web socket is already closed. Is that a race condition too taking into account that we aren't changing any variables? Is it going to happen at all and do I really need the try/catch that wraps the CloseOutputAsync, taking into account that no other part of my code closes the web socket?
I reused that SemaphoreSlim in StopAsync in order to prevent calling StartAsync and StopAsync at the same time, is that a good idea?
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();
}
}
CodePudding user response:
I would say that if you expect StopAsync
being called in parallel then it's worth having those measures around.
Though there is another potential flaw that I see in your code - you are sharing _clientWebSocket
and _tokenSource
between the start and stop so potentially you can end in next situation:
- Thread 1 calls
StartAsync
and initializes_clientWebSocket
and_tokenSource
and exists the semaphore - Thread 2 calls
StartAsync
and overwrites_clientWebSocket
and_tokenSource
- Thread 1
StopAsync
callsStopAsync
which will stopClientWebSocket
created by Thread 2 but no one will close the one created by Thread 1