Background validation of state, reset on user action

无人久伴 提交于 2020-07-07 10:28:36

问题


I'm really new to threading multitasking/multithreading, but I'm working on a project where I think I need it. The user will be editing a fairly complex diagram, and I want the program to check for validity of the diagram. The validity check is non-trivial (polynomial time, though, not NP - seconds, not minutes or years, but I don't want to hold the user up for a few seconds after every change) so I would like the program to check for validity in the background and highlight inconsistencies when it finds them. When the user makes certain changes to the diagram (changes the structure, not just the labels on elements), the validation will have to throw away what it was doing and start again. I'm assuming the user will eventually take a break to think/go for a pee/go for a coffee/chat to that rather cute person two cubicles along, but in case they don't, I have to let the validation run to completion in some circumstances (before a save or a printout, for example). Broad-brush, what are the features of C# I'll need to learn, and how do I structure that?


回答1:


Broad Brush. Here we go.

Q: "What are the features of C# I'll need to learn?"

A: You can get by nicely with a basic toolkit consisting (roughly speaking) of:

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

Q: "I don't want to hold the user up for a few seconds after every change"

A: OK, so we will never-ever block the UI thread. Fire off a Task to run a background validation routine that checks every now and then to see if it's been cancelled.

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);
}

We'll call CheckForRestart using GetAwaiter().OnCompleted(). This just means that without blocking we'll be notified as a callback when the thread finishes for one of three reasons:

  1. Cancelled
  2. Cancelled, but with an intent to start the validation over from the beginning.
  3. Ran validation to completion

By calling CheckForRestart we determine whether to start it over again or not.

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

Rather that post the complete code here, I pushed a simple working example to our GitHub. You can browse it there or clone and run it. 20-second screen capture. When the RESTART button is clicked in the video, it's checking the CurrentCount property of the Semaphore. In a threadsafe way it determines whether the validation routine is already running or not.

I hope I've managed to give you a few ideas about where to start. Sure, the explanation I've given here has a few holes but feel free to address your critical concerns in the comments and I'll try to respond.




回答2:


You probably need to learn about asynchronous programming with async/await, and about cooperative cancellation. The standard practice for communicating cancellation is by throwing an OperationCanceledException. Methods that are intended to be cancelable accept a CancellationToken as argument, and observe frequently the IsCancellationRequested method of the token. So here is the basic structure of a cancelable Validate method with a boolean result:

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;
}

The "driver" of the CancellationToken is a class named CancellationTokenSource. In your case you'll have to create multiple instances of this class, one for every time that the diagram is changed. You must store them somewhere so that you can call later their Cancel method, so lets make two private fields inside the Form, one for the most recent CancellationTokenSource, and one for the most recent validation Task:

private Task<bool> _validateTask;
private CancellationTokenSource _validateCTS;

Finally you'll have to write the logic for the event handler of the Diagram_Changed event. It is probably not desirable to have multiple validation tasks running side by side, so it's a good idea to await for the completion of the previous task before launching a new one. It is important that awaiting a task doesn't block the UI. This introduces the complexity that multiple Diagram_Changed events, along with other unrelated events, can occur before the completion of the code inside the handler. Fortunately you can count on the single-threaded nature of the UI, and not have to worry about the thread-safety of accessing the _validateTask and _validateCTS fields by multiple asynchronous workflows. You do need to be aware though that after every await these fields may hold different values than before the 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
}

The Validate method should not include any UI manipulation code, because it will be running in a background thread. Any effects to the UI should occur after the completion of the method, through the returned result of the validation task.



来源:https://stackoverflow.com/questions/62240030/background-validation-of-state-reset-on-user-action

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!