2

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?

stukselbax
  • 5,855
  • 3
  • 32
  • 54
  • Does this help? http://stackoverflow.com/questions/13513650/how-to-set-timeout-for-a-line-of-c-sharp-code/13513854 – Carsten May 08 '14 at 10:41
  • @Aschratt - No. I do not need TimeOut parameters. I need fast cancellation. – stukselbax May 08 '14 at 10:44
  • 1
    So, you cannot use `ThrowIfCancellationRequested` inside `StartSearchInternal`, can you? If so, check ["How do I cancel non-cancelable async operations?"](http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx). – noseratio May 08 '14 at 10:50
  • What about using Task.Factory.StartNew(() => Search(), TaskScheduler.Default).Wait(token)? You currently ignore the Task object returned by that function call. Also, how are you getting the IEnumerable return value that you actually want? – Evil Dog Pie May 08 '14 at 10:55
  • @Noseratio I can. I checking your link now. – stukselbax May 08 '14 at 10:59
  • @MikeofSST I don't put here the logic for use of search results - I thought that it is not so important. If I call Task.Wait(token), will it exit when either task finished or cancellation will be requested (but task is not cancelled yet, because throw clause is not called yet)? – stukselbax May 08 '14 at 11:05
  • You would put the Task.Wait(token) in a try{}catch(OperationCanceledException){} block. If you use a Task> type task, when the task completes, you retrieve the result from the Task.Result property. If it gets cancelled the catch block will run. – Evil Dog Pie May 08 '14 at 11:29
  • No need for wait handles. It is more convenient to do do all of this with tasks. – usr May 08 '14 at 11:58
  • I would accept @MikeofSST answer because its exactly what I need. – stukselbax May 08 '14 at 12:39

2 Answers2

2

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".

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • I forgot to mention that I use .net 4.0 – stukselbax May 08 '14 at 11:14
  • @stukselbax, then you have `Task.ContinueWith` to handle the task's completion, or you may still be able to use `async/await` with `Microsoft.Bcl.Async` library. There're [some other patterns](http://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx) as well, including `IEnumerator`/`yield`. Anyhow, you still should store the reference to the task somewhere. – noseratio May 08 '14 at 11:17
1

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();
        }
    }
}
Evil Dog Pie
  • 2,300
  • 2
  • 23
  • 46
  • I want to say that you are still responsible for exception catching inside the TPL task action – stukselbax May 12 '14 at 13:06
  • @stukselbax Would that be in the RealSearch method that gets called by the Task Action, or in the AwaitResultAsync method? I should maybe have duplicated the whole if(null==SearchResult){...} block in the AwaitResultAsync implementation. – Evil Dog Pie May 12 '14 at 20:04
  • 1
    that should be in the action that passed as a parameter to the TPL task, so that should be done in the RealSearch method. **[AFAIK](http://stackoverflow.com/questions/2707295/how-to-handle-all-unhandled-exceptions-when-using-task-parallel-library)**, in .net 4.0 if you didn't handle exceptions in the task - the application will fail, without useful information in Event Journal. In .net 4.5, this policy had been changed, so it is not the case. – stukselbax May 13 '14 at 04:12