Готовый перевод PATTERNS OF PARALLEL PROGRAMMING / ПАТТЕРНЫ ПАРАЛЛЕЛЬНОГО ПРОГРАММИРОВАНИЯ: ПЕРЕДАЧА ДАННЫХ
В .NET Framework существует несколько общепринятых способов передачи данных в асинхронную задачу.
ЗАМЫКАНИЯ
После того, как замыкания были введены в языки C# и Visual Basic, они стали простейшим способом передачи данных в фоновые операции. При обнаружении делегата, который ссылается на объект вне поля его видимости, компилятор преобразовывает переменные, к которым выполняется доступ, таким образом, чтобы они стали видны делегату, как бы "закрывая сверху" эти переменные. Благодаря этому механизму передача любого объема данных в фоновую задачу не представляет трудностей:
В приложениях, требующих предельной производительности и масштабируемости, важно помнить что внутри компилятор выделяет объект, в котором сохраняет переменные (в примере выше это data1 и data2), к которым обращается делегат.
ОБЪЕКТЫ СОСТОЯНИЯ
Обратившись к истокам .NET Framework, большинство API, порождающих асинхронные задачи, принимает состояние в качетстве параметра и передает это состояние в делегат, который и является телом задачи. ThreadPool.QueueUserWorkItem - главный тому пример:
Мы можем воспользоваться этим параметром для передачи состояния в WaitCallback:
Класс Task из .NET Framework 4 также поддерживает этот способ:
Обратите внимание, что в отличие от замыканий, в этом подходе обычно отсутствует выделение дополнительного объекта для хранения состояния, кроме случаев, когда состояние является типом значения (тип значение будет боксирован при передаче через параметр состояния).
Чтобы передать несколько порций данных таким способом, они должны быть обернуты в один объект. Раньше это обычно достигалось за счет дополнительного класса, который хранит эту информацию. В .NET Framework 4 можно использовать новый класс Tuple{}:
И при замыканиях и при работе с типами значений требуется выделение объекта для создания кортежа, оборачивающего составные элементы. Также встроенный в .NET Framework 4 тип кортежа имеет ограничение на хранимое количество единиц.
ОБЪЕКТЫ СОСТОЯНИЯ С МЕТОДАМИ
Еще один подход, схожий с предыдущим, заключается в передаче данных в асинхронную операцию в виде метода экземпляра класса, который и представляет асинхронную работу. Это позволяет явно передать данные в метод, используя this.
Этот способ также требует выделения объекта (экземпляр класса Work) для хранения состояния. Даже если сделать Work структурой этого выделения всё равно не избежать, поскольку создание делегата, ссылающегося на Work, требует хранения ссылки на экземпляр, у которого будет вызван метод Run. Эта ссылка хранится как объект, а значит структура будет боксирована.
А раз так, выбор конкретного подхода - в основном зависит от личных предпочтений. При использовании замыканий как правило получается более читаемый код, да и компилятор может оптимизировать создание объектов состояния. Например, если анонимная функция передаваемая в StartNew не использует локальное состояние, компилятор может не выделять объект для хранения состояния, поскольку оно уже хранится в виде доступного экземплярного или статического поля.
АНТИ-ПАТТЕРНЫ
ЗАМЫКАНИЕ СОВМЕСТНО ИСПОЛЬЗУЕМЫХ ДАННЫХ
Попробуйте угадать что выдаст приведенный код:
Скорей всего Вы ошиблись, если предположили, что код выдаст числа от 0 до 9. Хотя результат может оказаться и таким, вероятнее всего на выходе будет десять десяток. Это происходит из-за правил языка относительно областей видимости и способе захвата переменных анонимными методами, который здесь использовался для указания действия в QueueUserWorkItem. Переменная i совместно используется и основным потоком, который добавляет рабочие элементы, и потоками из пула потоков, которые печатают значение i. Основной поток последовательно обновляет значение i пока проходит от 0 до 9, и поэтому каждое напечатанная линия будет содержать значение i в момент вызова из фонового потока метода Console.WriteLine. (Учтите, что в отличие от компилятора C#, компилятор Visual Basic любезно предупреждает об этой проблеме: "warning BC42324: Using the iteration variable in a lambda expression may have unexpected results. Instead, create a local variable within the loop and assign it the value of the iteration variable.")
Этот феномен имеет место не только в параллельном программировании, хотя положение, занимаемое анонимными методами и лямбда-выражениями в модели параллельного программирования под .NET Framework, только усиливает проблему. Рассмотрим код последовательной реализации:
Достоверно можно утверждать, что будет напечатано десять десяток, поскольку на момент вызова делегатов значение i уже равно 10, а все делегаты ссылаются на одну и ту же захваченную переменную.
Мы можем избавиться от этой проблемы созданием локальной копии итерируемой переменной в области видимости цикла (чт, между прочим, и советовал компилятор Visual Basic). Тогда у каждого анонимного метода будет собственная переменная, а не общая для всех делегатов. Небольшое изменение исправляет предыдущий пример:
Теперь будет напечатана последовательность от 0 до 9, как и ожидалось. Аналогичным образом исправляется и параллельная реализация:
Этот код также напечатает цифры от 0 до 9, хотя именно такой порядок не гарантируется.
Привычка объявлять переменные в начале функции, а использовать их позже в замыканиях, также может Вас запутать . Например:
Следуя семантике замыканий, переменная j будет совместно использоваться всеми итерациями параллельного цикла тем самым нанося серьезный ущерб внутреннему последовательному циклу. Чтобы решить эту проблему следует объявление переменной переместить как можно ближе к использованию:
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется
Переведено на Нотабеноиде
http://notabenoid.org/book/16789/64472
Переводчики: vzhik
Настройки
Готово:
