Question

In order to perform long-running (let it be search in this context) operation, I put the loading logic inside a TPL task, so the general method Search() is called on background thread. Search() operation can be long enough, so I need the ability to cancel it properly, using the CancellationToken. But the Search() operation did not return until it finished, so I have to do some logic in order to implement convenient and (!) fast cancellation.

Using WaitHandle's I can implement something like this:

private void StartSearch() // UI thread
{
    CancellationTokenSource s = new CancellationTokenSource();
    Task.Factory.StartNew(() => StartSearchInternal(s.Token), s.Token)
}

private void StartSearchInternal(CancellationToken token) // Main Background Thread
{
    ManualResetEvent eHandle = new ManualResetEvent(false);
    Task.Factory.StartNew(() => Search(eHandle ), TaskScheduler.Default);
    WaitHandle.WaitAny(new [] { eHandle, token.WaitHandle });
    token.ThrowIfCancellationRequested();
}

private IEnumerable<object> Search(ManualResetEvent e) // Another Background thread
{
    try
    {
        // Real search call, i.e. to database, service, or AD, doesn't matter
        return RealSearch();
    }
    catch {} // Here, for simplicity of question, catch and eat all exceptions
    finally
    {
        try
        {
            e.Set();
        }
        catch {} 
    }
}

It's seems to me that this is not so elegant solution, that can be made.

Q: Is there any other approaches for this task?

Was it helpful?

Solution 2

This is my comment refactored into an answer containing code. It contains a couple of alternatives for using Task.Wait and the async pattern, the choice of which will depend on whether you call the method from the UI thread.

There are several comments to the O/P and other answers that contain valuable information regarding asynchronous behaviours. Please read these as the code below has many 'opportunities for improvement'.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace SearchAgent
{
    class CancellableSearchAgent
    {
        // Note: using 'object' is a bit icky - it would be better to define an interface or base class,
        // or at least restrict the type of object in some way, such as by making CancellableSearchAgent
        // a template CancellableSearchAgent<T> and replacing every copy of the text 'object' in this
        // answer with the character 'T', then make sure that the RealSearch() method return a collection
        // of objects of type T.
        private Task<IEnumerable<object>> _searchTask;
        private CancellationTokenSource _tokenSource;

        // You can use this property to check how the search is going.
        public TaskStatus SearchState
        {
            get { return _searchTask.Status; }
        }

        // When the search has run to completion, this will contain the result,
        // otherwise it will be null.
        public IEnumerable<object> SearchResult { get; private set; }

        // Create a new CancellableSearchAgent for each search.  The class encapsulates the 'workflow'
        // preventing issues with null members, re-using completed tasks, etc, etc.
        // You can add parameters, such as SQL statements as necessary.
        public CancellableSearchAgent()
        {
            _tokenSource = new CancellationTokenSource();
            _searchTask = Task<IEnumerable<object>>.Factory.StartNew(() => RealSearch(), TaskScheduler.Default);
        }

        // This method can be called from the UI without blocking.
        // Use this if the CancellableSearchAgent is part of your ViewModel (Presenter/Controller).
        public async void AwaitResultAsync()
        {
            SearchResult = await _searchTask;
        }

        // This method can be called from the ViewModel (Presenter/Controller), but will block the UI thread
        // if called directly from the View, making the UI unresponsive and unavailable for the user to
        // cancel the search.
        // Use this if CancellableSearchAgent is part of your Model.
        public IEnumerable<object> AwaitResult()
        {
            if (null == SearchResult)
            {
                try
                {
                    _searchTask.Wait(_tokenSource.Token);
                    SearchResult = _searchTask.Result;
                }
                catch (OperationCanceledException) { }
                catch (AggregateException)
                {
                    // You may want to handle other exceptions, thrown by the RealSearch() method here.
                    // You'll find them in the InnerException property.
                    throw;
                }
            }
            return SearchResult;
        }

        // This method can be called to cancel an ongoing search.
        public void CancelSearch()
        {
            _tokenSource.Cancel();
        }
    }
}

OTHER TIPS

If you have control over StartSearchInternal() and Search(eHandle), then you should be able to do cooperative cancellation with ThrowIfCancellationRequested inside your Search core loop.

For more details, I highly recommend reading this document: "Using Cancellation Support in .NET Framework 4".

On a side note, you should probably store a reference to the task returned by Task.Factory.StartNew(() => StartSearchInternal(s.Token), s.Token) somewhere in your ViewModel class. You most likely want to observe its result and any exception it may have thrown. You may want to check Lucian Wischik's "Async re-entrancy, and the patterns to deal with it".

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top