取消未按预期进行

Cancellation doesn't go as expected

while 循环当前阻塞线程,因为它会一直重试,直到连接建立。我希望它永远重试,但它不应该阻塞线程(就像我们调用 StartAsync 而不等待它一样)并使我们可以在 StartAsync 执行期间调用 StopAsync 并永远取消重试过程。它也需要将 CancellationToken 传递给 ConnectAsync

await client.StartAsync(); // do not block

await Task.Delay(5000);

await client.StopAsync();

我正在考虑在 while 循环之前移动 CancellationToken 并将其传递给 while 循环以及循环直到 while (!await ConnectAsync().ConfigureAwait(false) && !_tokenSource.IsCancellationRequested) 然后将该逻辑包装到 Task.Run 中以防止阻塞。你怎么看?

Full code (@GeneralClient2 class)

public async Task StartAsync()
{
    // Prevent a race condition
    await _semaphore.WaitAsync().ConfigureAwait(false);

    try
    {
        if (IsRunning)
        {
            return;
        }

        while (!await ConnectAsync().ConfigureAwait(false))
        {
        }

        IsRunning = true;

        Debug.Assert(_clientWebSocket != null);

        _tokenSource = new CancellationTokenSource();

        _processingSend = ProcessSendAsync(_clientWebSocket, _tokenSource.Token);
        _processingData = ProcessDataAsync(_tokenSource.Token);
        _processingReceive = ProcessReceiveAsync(_clientWebSocket);
    }
    finally
    {
        _semaphore.Release();
    }
}

public async Task StopAsync()
{
    if (!IsRunning)
    {
        return;
    }

    _logger.LogDebug("Stopping");

    try
    {
        if (_clientWebSocket is { State: not (WebSocketState.Aborted or WebSocketState.Closed or WebSocketState.CloseSent) })
        {
            await _clientWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false);
        }
    }
    catch
    {
    }

    await _processingReceive.ConfigureAwait(false);

    _logger.LogDebug("Stopped");
}

private async ValueTask<bool> ConnectAsync()
{
    _logger.LogDebug("Connecting");

    var ws = new ClientWebSocket();

    try
    {
        await ws.ConnectAsync(new Uri(_url), CancellationToken.None).ConfigureAwait(false);

        Connected?.Invoke(this, EventArgs.Empty);
    }
    catch (Exception) // WebSocketException or TaskCanceledException
    {
        ws.Dispose();
        return false;
    }

    _clientWebSocket = ws;

    _logger.LogDebug("Connected");

    return true;
}

The while loop currently blocks the thread

你确定吗?因为 ClientWebSocket.ConnectAsync 明确指出

This operation will not block. The returned Task object will complete after the connect request on the ClientWebSocket instance has completed.

所以 ConnectAsync 不应阻塞,因此 while 循环也不应阻塞。但即使它是 non-blocking,它仍可能消耗大量 CPU 的使用量。或者在你调用 ClientWebSocket.ConnectAsync 之前可能会抛出一些东西,因为你只是吃掉了你永远不会知道的所有异常。

make it possible for us to call StopAsync during StartAsync's execution and cancel that process of retrying forever

在线程或异步上下文中停止某些服务时应小心。由于其他一些任务可能会发现所需的资源已被释放。

while (!await ConnectAsync().ConfigureAwait(false) && !_tokenSource.IsCancellationRequested)

问题是如果没有建立连接,循环将退出并继续运行该方法。正因为如此,建议使用ThrowIfCancellationRequested,即使使用异常进行流量控制有些痛苦。

我的建议是让 StartAsync 使用一个中止连接过程的 cancellationToken。此方法应该 return 表示连接的对象,即 Task<MyConnection>。这个连接对象应该是一次性的。停止连接可以像

// Create connection
var cts = new CancellationTokenSource();
var myStartAsyncConnectionTask = StartAsync(cts.Token);

// Close connection
cts.Cancel();
try{
    var myConnection = await myStartAsyncConnectionTask;
    myConnection.Dispose();
}
catch(OperationCancelledException)
{
...
}

无论连接处于何种状态,这都应该有效。如果连接已建立,取消将不执行任何操作,对象将被释放。如果连接失败,等待任务应该抛出。请注意,需要编写 StartAsync 以便在该方法在任何阶段抛出任何异常时清除所有创建的资源。