Чтобы выполнить длительную (пусть это будет поиск в данном контексте) операцию, я помещаю логику загрузки в задачу TPL, поэтому общий метод Search () вызывается в фоновом потоке. Операция Search () может быть достаточно долгой, поэтому мне нужна возможность правильно ее отменить, используя CancellationToken . Но операция Search () не возвращалась, пока не завершилась, поэтому мне нужно выполнить некоторую логику, чтобы реализовать удобную и (!) Быструю отмену.

Используя WaitHandle Я могу реализовать что-то вроде этого:

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

Мне кажется, это не такое уж изящное решение, которое можно сделать.

В: Есть ли другие подходы к этой задаче?

2
stukselbax 8 Май 2014 в 14:39

2 ответа

Лучший ответ

Это мой комментарий, преобразованный в ответ, содержащий код. Он содержит несколько альтернатив для использования Task.Wait и шаблона async, выбор которых будет зависеть от того, вызываете ли вы метод из потока пользовательского интерфейса.

Есть несколько комментариев к O / P и другим ответам, которые содержат ценную информацию об асинхронном поведении. Прочтите их, поскольку в приведенном ниже коде есть много «возможностей для улучшения».

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();
        }
    }
}
1
Evil Dog Pie 8 Май 2014 в 14:12

Если у вас есть контроль над StartSearchInternal() и Search(eHandle), тогда вы сможете выполнять совместную отмену с ThrowIfCancellationRequested внутри вашего основного цикла Search.

Для получения дополнительных сведений я настоятельно рекомендую прочитать этот документ: «Использование поддержки отмены в .NET Framework 4».

Кстати, вам, вероятно, следует сохранить ссылку на задачу, возвращаемую Task.Factory.StartNew(() => StartSearchInternal(s.Token), s.Token) где-нибудь в вашем классе ViewModel. Скорее всего, вы захотите увидеть его результат и любое исключение, которое оно могло вызвать. Вы можете проверить " Асинхронный повторный вход и шаблоны для его решения ".

2
noseratio 8 Май 2014 в 11:50