如何在 C# 中正确 运行 和取消任务

How to correctly run and cancel a task in C#

我在 Visual Studio 2019 年使用 C#。我似乎无法正确 运行 并取消任务。我确定我没有抓住要点,但阅读大量文章只会产生更多问题。

编辑 --> 有关更短且更易于理解的示例,请参阅下文。

我正在使用 SerialPort 并希望它连续读取数据。我的 class COMDevice 有一些我可以用来测试的 public 属性。

public Task _RxTask;
public CancellationTokenSource _ctsRxTask = new CancellationTokenSource();
public CancellationToken _ctRxTask;

初始化过程完成后,我启动了一个持续读取 SerialPort 的异步方法。

public bool Initialize()
{
  ... Do some initialization stuff
  _ctRxTask = _ctsRxTask.Token;
  _RxTask = Task.Run(() => ReceiveCommandsAsync(_ctRxTask));

  Debug.WriteLine($"Initialize _RxTask.Status = {_RxTask.Status}");
}

ReceiveCommandsAsync 方法如下所示。不要太在意接收数据的处理(它基本上检查是否接收到序列“A\n”或“[Command]\n”)。

private async Task ReceiveCommandsAsync(CancellationToken ct)
{
    if (ct.IsCancellationRequested)
    {
        Debug.WriteLine("ct.IsCancellationRequested 1");
        return;
    }

    var buffer = new byte[1024];
    StringBuilder cmdReceived = new StringBuilder("", 256);

    try
    {
        while (_serialPort.IsOpen)
        {
            Debug.WriteLine($"ReceiveCommandAsync 1 _RxTask.Status = {_RxTask.Status}");

            int cnt = await _serialPort.BaseStream.ReadAsync(buffer, 0, 1024).ConfigureAwait(true);
            Debug.WriteLine($"ReceiveCommandAsync 2 _RxTask.Status = {_RxTask.Status}");
            cntMax = Math.Max(cnt, cntMax);

            for (int i = 0; i < cnt; i++)
            {
                if ((char)buffer[i] == '\n')
                {
                    if ((cmdReceived.Length == 1) && (cmdReceived[0] == 'A'))
                        mreAck.Set();
                    else if (cmdReceived.Length != 0)
                    { 
                        // Check if command matches
                        string s = cmdReceived.ToString().Substring(4);
                        if (!_RegisteredCmds.Contains(s))
                            Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} ReceiveCommandsAsync: cmd {cmdReceived} not found");
                    }
                    cmdReceived.Clear();
                }
                else
                    cmdReceived.Append((char)buffer[i]);
            }

            if (ct.IsCancellationRequested)
            {
                Debug.WriteLine("ct.IsCancellationRequested 2");
                return;
            }
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} ReceiveCommandsAsync: Exception {ex}");
    }
}

有几个问题:

首先,显示Task.Status的Debug.WriteLines一直在显示“WaitingForActivation”。但是我的任务是运行ning,因为当我连接的设备发送数据时,接收到数据。我不应该看到“运行”吗?

其次,在测试表单中,我将使用以下代码关闭 COMDevice:

private async void btnClose_ClickAsync(object sender, EventArgs e)
{
    _COMDevice?._ctsRxTask.Cancel();
    await Task.Delay(1000);
    _COMDevice?.Close();
    _COMDevice = null;
}

但是我的 ReceiveCommandsAsync 中的 ct.IsCancellationRequested 从未被触发。相反,我似乎收到一个异常:

20:12:02:446 ReceiveCommandsAsync: Exception System.IO.IOException: The I/O operation has been aborted because of either a thread exit or an application request.

我确定我在这里做错了很多事情!

编辑 - 附加测试

我创建了一个更加简化的测试。一个简单的 Windows 表单应用程序,它有 3 个按钮。一个用于启动任务,一个用于显示状态,一个用于停止任务。

好消息是我可以使用 CancellationToken 停止任务,所以我将调查为什么这有效,而上面的示例不起作用。

但是如果我显示状态,我会不断看到“WaitingForActivation”。我仍然不明白为什么这不是“运行”。取消任务后,状态显示为“RanToCompletion”,而不是“Cancelled”。

private void btnTestTask_Click(object sender, EventArgs e)
{
    cancellationToken = cancellationTokenSource.Token;

    myTask = Task.Run(() => Test(cancellationToken));
}

private void btnTestCancel_Click(object sender, EventArgs e)
{
    cancellationTokenSource.Cancel();
}

private void btnShowStatus_Click(object sender, EventArgs e)
{
    Debug.WriteLine($"Task Status: {myTask.Status}");
}

async static Task Test(CancellationToken cancellationToken)
{
    while (true)
    {
        try
        {
            Debug.WriteLine("The task is running");
            await Task.Delay(1000, cancellationToken);
        }
        catch (TaskCanceledException)
        {
            Debug.WriteLine("The task is cancelled");
            break;
        }
    }
}

First, the Debug.WriteLines showing the Task.Status are continuously showing "WaitingForActivation". But my task is running, because when my connected device sends data, the data is received. Shouldn't I see "Running" instead?

But if I show the status, I'm continuously seeing "WaitingForActivation". I still don't understand why this is not "Running". And after the task has been cancelled, the status shows "RanToCompletion", and not "Cancelled".

有两种类型的任务:委派任务和承诺任务。委托任务代表在某些上下文中执行的一些(同步)代码。 Promise 任务只是代表某种将要发生的“完成”。异步代码一般使用 Promise 任务。具体来说,从 async 方法返回的任务始终是 Promise 任务,从 Task.Run 返回的任务与 Task-返回委托一起使用也是如此。

由于 Promise 任务实际上并没有 delegate(即同步代码),只是代表某事的完成,因此它们永远不会进入 Running 状态。 WaitingForActivation 正常。 More detail on my blog.

But ct.IsCancellationRequested in my ReceiveCommandsAsync is never triggered. In the contrary, I seem to receive an Exception

实际上,我希望 IsCancellationRequested 设置得很好。问题是循环中的代码位于 ReadAsync,等待下一个字节进入串行端口。如果在设置取消标记后 中没有字节进入 ,那么您的代码将最终(异步地)等待 ReadAsync 而永远不会实际检查 IsCancellationRequested

理想情况下,您可以通过向下传递 CancellationToken 而不是进行显式检查来解决此问题。即,将 ct 直接传递到 ReadAsync。这就是我在博客中描述的 "90% case" for handling cancellation。这样 CancellationToken 将取消 ReadAsync 调用本身,非常类似于 CancellationToken 在您的简化示例中取消 Task.Delay 的方式。

我说“理想情况下”是因为这就是应该 工作的方式。但是,我怀疑这可能行不通。并非每个采用 CancellationToken 的方法实际上都尊重该标记。具体来说,我怀疑这可能行不通,因为 SerialPort 是一个古老的 class 并且旧版本的 .NET 支持 non-NT Windows 没有 a way to cancel specific I/O calls 的版本,所以除非 SerialPort 已更新为尊重 ReadAsync 中的 CancellationToken,我怀疑它可能会忽略它。

在那种情况下,您唯一真正的解决方案是关闭串行端口,作为副作用取消对该端口的 所有 I/O 操作。 (我将在以后的博客 post 我的 Cancellation 系列中介绍这项技术)。这个 一个取消,虽然它没有用 OperationCanceledException 表示 - 再说一遍,因为 SerialPortOperationCanceledException 老得多,而且微软有一个非常高 backwards-compatibility 栏。

对不起,文字墙; TL;DR 是:

  1. 尝试将 CancellationToken 传递给 ReadAsync
  2. 如果这不起作用,请关闭端口而不是使用取消令牌,并忽略关闭端口后发生的任何异常。