С# Асинхронные параметры обработки списка

Я пытаюсь лучше понять параметры Async и Parallel, которые у меня есть на С#. В нижеприведенных фрагментах я включил 5 подходов, которые я встречал больше всего. Но я не уверен, что выбрать - или еще лучше, какие критерии следует учитывать при выборе:

Метод 1: Задача

(см. http://msdn.microsoft.com/en-us/library/dd321439.aspx)

Вызов StartNew функционально эквивалентен созданию задачи с использованием одного из ее конструкторов, а затем вызывает Start, чтобы запланировать ее выполнение. Однако, если создание и планирование не должны быть разделены, StartNew - это рекомендуемый подход как для простоты, так и для производительности.

TaskFactory. Метод StartNew должен быть предпочтительным механизмом для создания и планирования вычислительных задач, но для сценариев, в которых создание и планирование должны быть разделены, могут использоваться конструкторы, а затем метод запуска задачи может использоваться для планирования выполнения задачи в более позднее время.

// using System.Threading.Tasks.Task.Factory
void Do_1()
{
    var _List = GetList();
    _List.ForEach(i => Task.Factory.StartNew(_ => { DoSomething(i); }));
}

Метод 2: QueueUserWorkItem

(см. http://msdn.microsoft.com/en-us/library/system.threading.threadpool.getmaxthreads.aspx)

Вы можете поставить в очередь столько запросов пула потоков, сколько позволяет системная память. Если запросов больше, чем потоков пула потоков, дополнительные запросы остаются в очереди до тех пор, пока потоки пула потоков не станут доступными.

Вы можете поместить данные, требуемые методом очереди, в поля экземпляра класса, в котором этот метод определен, или вы можете использовать перегрузку QueueUserWorkItem (WaitCallback, Object), которая принимает объект, содержащий необходимые данные.

// using System.Threading.ThreadPool
void Do_2()
{
    var _List = GetList();
    var _Action = new WaitCallback((o) => { DoSomething(o); });
    _List.ForEach(x => ThreadPool.QueueUserWorkItem(_Action));
}

Метод 3: Параллельный .Foreach

(см.: http://msdn.microsoft.com/en-us/library/system.threading.tasks.parallel.foreach.aspx)

Класс Parallel предоставляет параллельные замены данных на основе библиотек для общих операций, таких как для циклов, для каждой петли и выполнения набора инструкций.

Делегатор тела вызывается один раз для каждого элемента в источнике, который можно перечислить. Он снабжен текущим элементом как параметром.

// using System.Threading.Tasks.Parallel
void Do_3()
{
    var _List = GetList();
    var _Action = new Action<object>((o) => { DoSomething(o); });
    Parallel.ForEach(_List, _Action);
}

Метод 4: IAsync.BeginInvoke

(см.: http://msdn.microsoft.com/en-us/library/cc190824.aspx)

BeginInvoke является асинхронным; поэтому управление немедленно возвращается к вызывающему объекту после его вызова.

// using IAsync.BeginInvoke()
void Do_4()
{
    var _List = GetList();
    var _Action = new Action<object>((o) => { DoSomething(o); });
    _List.ForEach(x => _Action.BeginInvoke(x, null, null));
}

Метод 5: BackgroundWorker

(см.: http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx)

Чтобы настроить фоновую операцию, добавьте обработчик событий для события DoWork. Вызовите свою трудоемкую операцию в этом обработчике событий. Чтобы запустить операцию, вызовите RunWorkerAsync. Чтобы получать уведомления об обновлениях прогресса, обработайте событие ProgressChanged. Чтобы получать уведомление, когда операция завершена, обработайте событие RunWorkerCompleted.

// using System.ComponentModel.BackgroundWorker
void Do_5()
{
    var _List = GetList();
    using (BackgroundWorker _Worker = new BackgroundWorker())
    {
        _Worker.DoWork += (s, arg) =>
        {
            arg.Result = arg.Argument;
            DoSomething(arg.Argument);
        };
        _Worker.RunWorkerCompleted += (s, arg) =>
        {
            _List.Remove(arg.Result);
            if (_List.Any())
                _Worker.RunWorkerAsync(_List[0]);
        };
        if (_List.Any())
            _Worker.RunWorkerAsync(_List[0]);
    }
}

Я полагаю, что очевидная критика будет:

  • Лучше, чем другой для производительности?
  • Лучше, чем другой для обработки ошибок?
  • Лучше, чем другой для мониторинга/обратной связи?

Но как выбрать вы? Заранее благодарим за понимание.

Ответ 1

В первом, третьем и четвертом примерах использование ThreadPool неявно, потому что по умолчанию задачи запланированы на ThreadPool, а расширения TPL используют ThreadPool, API просто скрывает некоторую сложность, см. здесь и здесь. BackgroundWorkers являются частью пространства имен ComponentModel, поскольку они предназначены для использования в сценариях пользовательского интерфейса.

Ответ 2

Переходя к ним в произвольном порядке:

BackgroundWorker (# 5)
Мне нравится использовать BackgroundWorker, когда я делаю что-то с пользовательским интерфейсом. Преимущество, которое у него есть, заключается в том, что в потоке пользовательского интерфейса запускаются события прогресса и завершения, что означает, что вы не получаете неприятных исключений при попытке изменить элементы пользовательского интерфейса. Он также имеет хороший встроенный способ отчетности о прогрессе. Один из недостатков этого режима заключается в том, что если у вас есть блокировка вызовов (например, веб-запросов) в вашей работе, у вас будет поток, который ничего не будет делать, пока происходит работа. Это, вероятно, не проблема, если вы только думаете, что у вас будет несколько из них.

IAsyncResult/Begin/End (APM, # 4)
Это широко распространенная и мощная, но сложная модель. Обработка ошибок затруднительна, так как вам нужно перехватывать исключения при вызове "Завершить", а неперехваченные исключения не обязательно возвращают к каким-либо соответствующим фрагментам кода, которые могут его обработать. Это может привести к постоянному зависанию запросов в ASP.NET или просто к ошибкам, которые таинственно исчезают в других приложениях. Вы также должны проявлять бдительность в отношении свойства CompletedSynchronously. Если вы не отслеживаете и не сообщаете об этом должным образом, программа может зависать и утечка ресурсов. Оборотная сторона этого заключается в том, что если вы работаете внутри контекста другого APM, вы должны убедиться, что любые методы async, которые вы вызываете, также сообщают об этом значении. Это означает, что нужно сделать еще один вызов APM или использовать Task и передать его в IAsyncResult, чтобы получить его свойство CompletedSynchronously.

В подписях также много накладных расходов: вам нужно поддерживать произвольный объект, чтобы выполнить его, сделать свою собственную реализацию IAsyncResult, если вы пишете метод асинхронного поиска, который поддерживает опросы и механизмы ожидания (даже если вы только используя обратный вызов). Кстати, вы должны использовать только обратный вызов. Когда вы используете маркер ожидания или опрос IsCompleted, вы теряете поток во время ожидания операции.

Асинхронный шаблон на основе событий (EAP)
Тот, который не был в вашем списке, но я хочу упомянуть о полноте. Это немного более дружелюбно, чем APM. Есть события вместо обратных вызовов, и там меньше мусора, висящего на сигнатурах метода. Обработка ошибок немного проще, так как она сохраняется и доступна в обратном вызове, а не повторно бросается. CompletedSynchronously также не является частью API.

Задачи (# 1)
Задачи - еще один дружественный асинхронный API. Обработка ошибок проста: исключение всегда существует для проверки обратного вызова, и никто не заботится о завершенном синхронном режиме. Вы можете делать зависимости, и это отличный способ справиться с выполнением нескольких задач async. Вы можете даже перенести APM или EAP (один тип, который вы пропустили) асинхронные методы в них. Еще одна хорошая вещь об использовании задач - ваш код не заботится о том, как выполняется операция. Он может блокироваться в потоке или быть полностью асинхронным, но потребительский код не заботится об этом. Вы также можете легко смешивать операции APM и EAP с задачами.

Параллельные методы. (# 3)
Это дополнительные помощники поверх задач. Они могут выполнить часть работы по созданию задач для вас и сделать ваш код более читабельным, если ваши задачи асинхронного программирования подходят для работы в цикле.

ThreadPool.QueueUserWorkItem(# 2)
Это низкоуровневая утилита, которая фактически используется ASP.NET для всех запросов. У него нет встроенных функций обработки ошибок, таких как задачи, поэтому вам нужно поймать все и передать их обратно в свое приложение, если вы хотите узнать об этом. Он подходит для работы с интенсивным процессором, но вы не хотите накладывать на него какие-либо блокирующие вызовы, такие как синхронный веб-запрос. Это потому что, пока он работает, он использует поток.

async/await Ключевые слова
Новые в .NET 4.5, эти ключевые слова позволяют писать асинхронный код без явных обратных вызовов. Вы можете ждать на Task и любом коде ниже, он будет ждать завершения этой операции async, не потребляя поток.

Ответ 3

Реактивные расширения - еще одна предстоящая библиотека для обработки асинхронного программирования, особенно когда речь идет о составе асинхронных событий и методов.

Это не родной, однако он был разработан лабораториями Ms. Он доступен как для .NET 3.5, так и для .NET 4.0 и представляет собой, по существу, набор методов расширения на .NET 4.0, представленном интерфейсом IObservable<T>.

На главном сайте много примеров и руководств, и я настоятельно рекомендую проверить некоторые из них. Сначала шаблон может показаться немного странным (по крайней мере, для программистов .NET), но он того стоит, даже если он просто захватывает новую концепцию.

Реальная сила реактивных расширений (Rx.NET) - это когда вам нужно собрать несколько асинхронных источников и событий. Все операторы разработаны с учетом этого и обрабатывают уродливые части асинхронности для вас.

Основной сайт: http://msdn.microsoft.com/en-us/data/gg577609

Руководство для начинающих: http://msdn.microsoft.com/en-us/data/gg577611

Примеры: http://rxwiki.wikidot.com/101samples

Тем не менее, лучший асинхронный шаблон, вероятно, зависит от того, в какой ситуации вы находитесь. Некоторые из них лучше (проще) для более простых вещей, а некоторые более расширяемы и легче обрабатываются, когда речь идет о более сложных сценариях. Я не могу говорить за все, о чем вы упоминаете.

Ответ 4

Последний, по крайней мере, лучший для 2,3. Для этого у него есть встроенные методы/свойства. Другие варианты почти одинаковы, просто разные версии/удобные обертки