Multi-Threading Cross-Class Cancellation with TPL

2019-04-02 19:37发布

问题:

All, I have a long running process that I run on a background thread (with cancellation support) using the Task Paralell Library (TPL). The code for this long running taks is contained within Class Validation, and when the method

public bool AsyncRunValidationProcess(TaskScheduler _uiScheduler, 
    CancellationToken _token, dynamic _dynamic = null)
{
    try
    {

        // Note: _uiScheduler is used to update the UI thread with progress infor etc.

        for (int i = 0; i < someLargeLoopNumber; i++)
        {
            // Cancellation requested from UI Thread.
            if (_token.IsCancellationRequested) 
                _token.ThrowIfCancellationRequested();
        }
        return true;
    }
    catch (Exception eX)
    {
        // Do stuff. Display `eX` if cancellation requested.
        return false;
    }
}

is run from Class Validation I can cancel the process fine. The cancellation request is handled by the appropriate delegate (shown below) and this works fine (I don't belive this is the cause of my problem).

When I run this method from another class, Class Batch, I do this via a "controller" method

asyncTask = Task.Factory.StartNew<bool>(() => asyncControlMethod(), token);

which in turn invokes the method

valForm.AsyncRunValidationProcess(uiScheduler, token, 
    new List<string>() { strCurrentSiteRelPath }));

where valForm is my accessor to Class Validation, the method runs fine, but when I attempt a cancellation the delegate

cancelHandler = delegate 
{
    UtilsTPL.CancelRunningProcess(asyncTask, cancelSource);
};

where

public static void CancelRunningProcess(Task _task, 
    CancellationTokenSource _cancelSource)
{
    try
    {
        _cancelSource.Cancel();
        _task.Wait(); // On cross-class call it freezes here.
    }
    catch (AggregateException aggEx)
    {
        if (aggEx.InnerException is OperationCanceledException)
            Utils.InfoMsg("Operation cancelled at users request.");
        if (aggEx.InnerException is SqlException)
            Utils.ErrMsg(aggEx.Message);
    }
}

freezes/hangs (with no unhandled exception etc.) on _task.Wait(). This (I belive - through testing) is to do with the fact that I am cancelling asyncControlMethod() which has called valForm.AsyncRunValidationProcess(...), so it is cancelling asyncControlMethod() which is causing the current process to hang. The problem seems to be with passing the CancellationTokenSource etc. to the child method. The IsCancellationPending event fires and kills the controlling method, which causes the child method to hang.

Can anyone tell me what I am doing wrong or (more pertinently), what should I be doing to allow such a cancellation procedure?

Note: I have tried to spawn a child task to run valForm.AsyncRunValidationProcess(...), with its own CancellationToken but this has not worked.

Thanks for your time.

回答1:

The answer to this problem (helped massively by Jiaji Wu's comment and link) was that you cannot declare the CancellationToken as a global variable that is passed to the cascading methods; that is, you cannot have

public class MainClass
{
    private CancellationTokenSource = source;
    private CancellationToken token;

    public MainClass()
    {
        source = new CancellationtokenSource();
        token = source.Token;
    }

    private void buttonProcessSel_Click(object sender, EventArgs e)
    {
        // Spin-off MyMethod on background thread.
        Task<bool> asyncControllerTask = null;
        TaskSpin(asyncControllerTask, cancelSource, token, MyMethod);
    }

    private void method()
    {
        // Use the global token DOES NOT work!
        if (token.IsCancellationRequested)      
            token.ThrowIfCancellationRequested();
    }

    private void TaskSpin(Task<bool> asyncTask, CancellationTokenSource cancelSource,
        CancellationToken token, Func<bool> asyncMethod)
    {
        try
        {
            token = cancelSource.Token;
            asyncTask = Task.Factory.StartNew<bool>(() => asyncMethod(token), token);

            // To facilitate multitasking the cancelTask ToolStripButton
            EventHandler cancelHandler = null;
            if (cancelSource != null)
            {
                cancelHandler = delegate
                {
                    UtilsTPL.CancelRunningProcess(mainForm, uiScheduler, asyncTask, cancelSource, true);
                };
            }

        // Callback for finish/cancellation.
            asyncTask.ContinueWith(task =>
            {
                // Handle cancellation etc.
            }

            // Other stuff...
        }
    }
}

Use of the global token in the maethod run on the background thread doen NOT work! The method must be explicitly passed the token for it to be able to register it. I am not sure of the exact reason why this is the case, but I will know in future, now you need to pass the token to MyMethod() like this

    private void method(CancellationToken token)
    {
        // Use the global token DOES NOT work!
        if (token.IsCancellationRequested)      
            token.ThrowIfCancellationRequested();
    }

I hope this helps someone else.