表单方法使用取消令牌停止任务并等待任务完成。停止方法永不屈服,任务也不会停止

Form method stops Task with cancellation token and waits for task to complete. Stop Method never yields and Task does not stop

我有一项服务 Class 可以启动和停止一项任务,该任务旨在 运行 在表单应用程序的后台进行。

停止监控时: 一旦调用了 Stop() 方法,Monitor Task 上就没有断点,并且 IsStarted 永远不会变为 false' 并陷入无限循环。

这有什么问题吗?为什么监控任务似乎停止了 运行ning?我该如何解决这个问题?


        private volatile bool IsStarted = false;

        public void Start(PartList partList, UnioServiceSettings settings, Action<string, string, string> createSummaryFile)
        {
            _Settings = settings;
            _PartList = partList;
            _cts = new CancellationTokenSource();
            Task.Run(async() => await Monitor(_cts.Token));  //no difference:.ConfigureAwait(false);
            _ProgressMessage?.Report("Monitor Started...");
            IsStarted = true;
        }
            
        public async Task<bool> Stop()
        {
            if (IsStarted)
            {
                _ProgressMessage?.Report("Stopping Monitor...");
                _cts.Cancel();
                while (IsStarted) //this loop runs forever
                {
                    await Task.Delay(1000);  //.ConfigureAwait(false);? no difference
                }
                return true;
            }
            else
            {
                _ProgressMessage?.Report("Cannot stop Monitor. It has not been started.");
                return false;
            }
        }

        private async Task Monitor(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                 //do stuff
                 await Task.Delay(_Settings.DelayMs,token).ConfigureAwait(false);
            }
            IsStarted = false;
            return;
        }

这里存在竞争条件:

Task.Run(async() => await Monitor(_cts.Token));
_ProgressMessage?.Report("Monitor Started...");
IsStarted = true;

Task.Run 方法创建的 Task 有可能在当前线程调用 IsStarted = true; 之前在另一个线程上完成。所以最终 IsStarted 将稳定在值 true 而没有 Task 运行。要解决此问题,您可以将更改 IsStarted 字段的责任专门转移到 Monitor 方法本身:

private async Task Monitor(CancellationToken token)
{
    IsStarted = true;
    try
    {
        _ProgressMessage?.Report("Monitor Started...");
        while (true)
        {
            token.ThrowIfCancellationRequested();
            //do stuff
            await Task.Delay(_Settings.DelayMs,token).ConfigureAwait(false);
        }
    }
    finally
    {
        IsStarted = false;
    }
}

不过,我不知道这种竞争条件是否是导致您出现问题的原因。


更新: 这是实现服务的另一种方法,其中 bool IsStarted 字段已替换为 Task _monitorTask 字段。此实现假设调用者不会同时调用方法 StartStop。在 SemaphoreSlim(1, 1).

的帮助下,它实际上执行了 no-concurrency 策略
private readonly SemaphoreSlim _semaphore = new(1, 1);
private CancellationTokenSource _cts;
private Task _monitorTask;
private IProgress<string> _progressMessage;

public void Start()
{
    if (!_semaphore.Wait(0)) throw new InvalidOperationException("Concurrency!");
    try
    {
        if (_monitorTask != null && !_monitorTask.IsCompleted)
            throw new InvalidOperationException("The Monitor is already running!");
        _progressMessage?.Report("Starting Monitor...");
        _cts = new();
        CancellationToken token = _cts.Token;
        _monitorTask = Task.Run(() => MonitorAsync(token), token);
    }
    finally { _semaphore.Release(); }
}

public async Task<bool> Stop()
{
    if (!_semaphore.Wait(0)) throw new InvalidOperationException("Concurrency!");
    try
    {
        if (_monitorTask == null)
        {
            _progressMessage?.Report("Monitor has not been started.");
            return false;
        }
        else if (_monitorTask.IsCompleted)
        {
            _progressMessage?.Report("Monitor is already stopped.");
            return false;
        }
        else
        {
            _progressMessage?.Report("Stopping Monitor...");
            _cts?.Cancel();
            try { await _monitorTask; } catch { } // Ignore exceptions here
            _cts?.Dispose(); _cts = null;
            return true;
        }
    }
    finally { _semaphore.Release(); }
}

private async Task MonitorAsync(CancellationToken token)
{
    try
    {
        _progressMessage?.Report("Monitor started.");
        while (true)
        {
            // Do stuff
            await Task.Delay(1000, token);
        }
    }
    catch (OperationCanceledException) when (token.IsCancellationRequested)
    {
        _progressMessage?.Report("Monitor stopped.");
    }
    catch (Exception ex)
    {
        _progressMessage?.Report($"Monitor failed: {ex.Message}");
    }
}

请注意,_cts.Token 存储在 Start 方法内的局部变量中,以防止可能的竞争条件。 Task.Run 操作在另一个线程上调用,很可能是在 Start 方法完成之后。我们不希望此操作与 _cts 字段交互,该字段必须仅从 concurrency-protected 区域处理。