【问题标题】:Background validation of state, reset on user action状态的后台验证,用户操作重置
【发布时间】:2020-06-07 01:55:39
【问题描述】:

我对线程多任务/多线程真的很陌生,但我正在做一个我认为我需要它的项目。用户将编辑一个相当复杂的图表,我希望程序检查图表的有效性。有效性检查很重要(多项式时间,不是 NP - 秒,不是分钟或年,但我不想在每次更改后让用户等待几秒钟)所以我希望程序检查在后台验证有效性,并在发现不一致时突出显示它们。当用户对图表进行某些更改(更改结构,而不仅仅是元素上的标签)时,验证将不得不放弃它正在做的事情并重新开始。我假设用户最终会休息一下来思考/去小便/去喝咖啡/和那个相当可爱的人聊天两个隔间,但如果他们不这样做,我 在某些情况下让验证运行完成(例如,在保存或打印输出之前)。粗略,我需要学习 C# 的哪些特性,以及如何构建它?

【问题讨论】:

    标签: c# multithreading asynchronous


    【解决方案1】:

    宽刷。我们开始吧。

    问:“我需要学习 C# 的哪些特性?”

    答:你可以使用一个基本的工具包(粗略地说):

    System.Threading.Tasks.Task
    System.Threading.CancellationTokenSource
    System.Threading.SemaphoreSlim
    

    问:“我不想在每次更改后让用户停留几秒钟”

    A:好的,所以我们永远不会阻塞 UI 线程。触发任务以运行后台验证例程,该例程不时检查它是否已被取消。

    CancellationTokenSource _cts = null;
    SemaphoreSlim ssBusy = new SemaphoreSlim(2);
    private void ExecValidityCheck()
    {
        ssBusy.Wait();
        Task.Run(() =>
        {
            try
            {
                _cts = new CancellationTokenSource();
                LongRunningValidation(_cts.Token);
            }
            finally
            {
                ssBusy.Release();
            }
        })
        .GetAwaiter()
        .OnCompleted(CheckForRestart);
    }
    

    我们将使用 GetAwaiter().OnCompleted() 调用 CheckForRestart。这只是意味着没有阻塞当线程由于以下三个原因之一完成时,我们将作为回调通知:

    1. 已取消
    2. 已取消,但打算从头开始验证。
    3. 运行验证完成

    通过调用CheckForRestart,我们决定是否重新开始。

    void CheckForRestart()
    {
        BeginInvoke((MethodInvoker)delegate
        {
            if (_restart)
            {
                _restart = false;
                ExecValidityCheck();
            }
            else
            {
                buttonCancel.Enabled = false;
            }
        });
    }
    

    与其在这里发布完整代码,我将一个简单的working example 推送到我们的 GitHub。您可以在那里浏览它或克隆并运行它。 20-second screen capture。在视频中单击 RESTART 按钮时,它会检查 Semaphore 的 CurrentCount 属性。它以线程安全的方式确定验证例程是否已经在运行。

    我希望我已经设法为您提供了一些关于从哪里开始的想法。当然,我在这里给出的解释有一些漏洞,但请随时解决您在 cmets 中的关键问题,我会尽力回应。

    【讨论】:

    • @digitig thx!我不想在答案中添加太多内容,但是……您可能需要两个看门狗(倒计时)计时器。
    • 1. 你提到如果用户活动是这样的,验证永远可以一直运行,那么最终你会强制旋转等待光标并允许每隔一段时间完成验证。 WDT 设置为 5 M(或其他)倒计时开始在活动的第一个迹象,并且仅在验证完成时重置。
    • 2. 如果用户正在疯狂地打字或点击并且小便(或者可能同时进行)为什么还要开始验证当您知道它只需要在 50 毫秒内重新启动时?因此,在尝试运行后台任务之前,请等待一两秒钟。祝你好运!
    【解决方案2】:

    您可能需要了解asynchronous programming with async/awaitcooperative cancellation。沟通取消的标准做法是抛出OperationCanceledException。可以取消的方法接受CancellationToken 作为参数,并经常观察令牌的IsCancellationRequested 方法。所以这里是一个带有布尔结果的可取消Validate 方法的基本结构:

    bool Validate(CancellationToken token)
    {
        for (int i = 0; i < 50; i++)
        {
            // Throw an OperationCanceledException if cancellation is requested
            token.ThrowIfCancellationRequested();
            Thread.Sleep(100); // Simulate some CPU-bound work
        }
        return true;
    }
    

    CancellationToken 的“驱动程序”是一个名为 CancellationTokenSource 的类。在您的情况下,您必须创建此类的多个实例,每次更改图表时创建一个。您必须将它们存储在某个地方,以便以后可以调用它们的Cancel 方法,因此让我们在Form 中创建两个私有字段,一个用于最新的CancellationTokenSource,一个用于最新的验证Task

    private Task<bool> _validateTask;
    private CancellationTokenSource _validateCTS;
    

    最后,您必须为Diagram_Changed 事件的事件处理程序编写逻辑。可能不希望同时运行多个验证任务,因此最好在启动新任务之前发送await 以完成前一个任务。等待任务不会阻塞 UI,这一点很重要。这引入了复杂性,多个Diagram_Changed 事件以及其他不相关的事件可能在处理程序内的代码完成之前发生。幸运的是,您可以依靠 UI 的单线程特性,而不必担心通过多个异步工作流访问 _validateTask_validateCTS 字段的线程安全性。您确实需要注意,在每个 await 之后,这些字段可能包含与 await 之前不同的值。

    private async void Diagram_Changed(object sender, EventArgs e)
    {
        bool validationResult;
        using (var cts = new CancellationTokenSource())
        {
            _validateCTS?.Cancel(); // Cancel the existing CancellationTokenSource
            _validateCTS = cts; // Publish the new CancellationTokenSource
            if (_validateTask != null)
            {
                // Await the completion of the previous task before spawning a new one
                try { await _validateTask; }
                catch { } // Ignore any exception
            }
            if (cts != _validateCTS) return; // Preempted (the event was fired again)
    
            // Run the Validate method in a background thread
            var task = Task.Run(() => Validate(cts.Token), cts.Token);
            _validateTask = task; // Publish the new task
    
            try
            {
                validationResult = await task; // Await the completion of the task
            }
            catch (OperationCanceledException)
            {
                return; // Preempted (the validation was canceled)
            }
            finally
            {
                // Cleanup before disposing the CancellationTokenSource
                if (_validateTask == task) _validateTask = null;
                if (_validateCTS == cts) _validateCTS = null;
            }
        }
        // Do something here with the result of the validation
    }
    

    Validate 方法不应包含任何 UI 操作代码,因为它将在后台线程中运行。对 UI 的任何影响都应该在方法完成后通过验证任务的返回结果发生。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-01-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-21
      相关资源
      最近更新 更多