Асинхронные делегаты. Асинхронность в программировании Модель асинхронного программирования

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

Первый пример - сетевой сервер, веб-приложение. Чаще всего как таковых вычислений на процессоре такие приложения не выполняют. Большая часть времени (реального, не процессорного) тратится на ввод-вывод: чтение запроса от клиента, обращение к диску за данными, сетевые обращение к другим подсистемам (БД, кэширующие сервера, RPC и т.п.), запись ответа клиенту. Во время этих операций ввода-вывода процессор простаивает, его можно загрузить обработкой запросов других клиентов. Возможны различные способы решить эту задачу: отдельный процесс на каждое соединение (Apache mpm_prefork, PostgreSQL , PHP FastCGI), отдельный поток (нить) на каждое соединение или комбинированный вариант процесс/нить (Apache mpm_worker, MySQL). Подход с использованием процессов или нитей перекладывает мультиплексирование процессора между обрабатываемыми соединениями на ОС, при этом расходуется относительно много ресурсов (память, переключения контекста и т.п.), такой вариант не подходит для обработки большого количества одновременных соединений, но идеален для ситуации, когда объем вычислений достаточно высок (например, в СУБД). К плюсам модели нитей и процессов можно добавить потенциальное использование всех доступных процессоров в многопроцессорной архитектуре.

Альтернативой является использование однопоточной модели с использованием примитивов асинхронного ввода-вывода, предоставляемых ОС (select, poll, и т.п.). При этом объем ресурсов на каждое новое обслуживаемое соединение не такой большой (новый сокет, какие-то структуры в памяти приложения). Однако программирование существенно усложняется, т.к. данные из сетевых сокетов поступают некоторыми “отрывками”, причем за один цикл обработки данные поступают от разных соединений, находящихся в разных состояниях, часть соединений могут быть входящими от клиентов, часть - исходящими к внешним ресурсам (БД, другой сервер и т.п.). Для упрощения разработки используются различные концепции: callback, конечные автоматы и другие. Примеры сетевых серверов, использующих асинхронный ввод-вывод: nginx , lighttpd , HAProxy , pgBouncer , и т.д. Именно при такой однопоточной модели возникает необходимость в асинхронном программировании. Например, мы хотим выполнить запрос в БД. С точки зрения программы выполнение запроса - это сетевой ввод-вывод: соединение с сервером, отправка запроса, ожидание ответа, чтение ответа сервера БД. Поэтому если мы вызываем функцию “выполнить запрос БД”, то она сразу вернуть результат не сможет (иначе она должна была бы заблокироваться), а вернет лишь нечто, что позволит впоследствие получить результат запроса или, возможно, ошибку (нет соединения с сервером, некорректный запрос и т.п.) Этим возвращаемым значением удобно сделать именно Deferred.

Второй пример связан с разработкой обычных десктопных приложений. Предположим, мы решили сделать аналог Miranda (QIP , MDC , …), то есть свой мессенджер. В интерфейсе программы есть контакт-лист, где можно удалить контакт. Когда пользователь выбирает это действие, он ожидает что контакт исчезнет на экране и что он действительно удалится из контакт-листа. На самом деле операция удаления из серверного контакт-листа опирается на сетевое взаимодействие с сервером, при этом пользовательский интерфейс не должен быть заблокирован на время выполнения этой операции, поэтому в любом случае после выполнения операции потребуется некоторое асинхронное взаимодействие с результатом операции. Можно использовать механизм сигналов-слотов, callback’ов или что-то еще, но лучше всего подойдет Deferred: операция удаления из контакт-листа возвращает Deferred, в котором обратно придет либо положительный результат (всё хорошо), либо исключение (точная ошибка, которую надо сообщить пользователю): в случае ошибки контакт надо восстановить контакт в контакт-листе.

Примеры можно приводить долго и много, теперь о том, что же такое Deferred. Deferred - это сердце framework’а асинхронного сетевого программирования Twisted в Python. Это простая и стройная концепция, которая позволяет перевести синхронное программирование в асинхронный код, не изобретая велосипед для каждой ситуации и обеспечивая высокое качества кода. Deferred - это просто возвращаемый результат функции, когда этот результат неизвестен (не был получен, будет получен в другой нити и т.п.) Что мы можем сделать с Deferred? Мы можем “подвеситься” в цепочку обработчиков, которые будут вызваны, когда результат будет получен. При этом Deferred может нести не только положительный результат выполнения, но и исключения, сгенерированные функцией или обработчиками, есть возможность исключения обработать, перевыкинуть и т.д. Фактически, для синхронного кода есть более-менее однозначная параллель в терминах Deferred. Для эффективной разработки с Deferred оказываются полезными такие возможности языка программирования, как замыкания, лямбда-функци.

Приведем пример синхронного кода и его альтернативу в терминах Deferred:

Try: # Скачать по HTTP некоторую страницу page = downloadPage(url) # Распечатать содержимое print page except HTTPError, e: # Произошла ошибка print "An error occured: %s", e
В асинхронном варианте с Deferred он был бы записан следующим образом:

Def printContents(contents): """ Callback, при успешном получении текста страницы, распечатываем её содержимое. """ print contents def handleError(failure): """ Errback (обработчик ошибок), просто распечатываем текст ошибки. """ # Мы готовы обработать только HTTPError, остальные исключения # "проваливаются" ниже. failure.trap(HTTPError) # Распечатываем само исключение print "An error occured: %s", failure # Теперь функция выполняется асинхронно и вместо непосредственного # результата мы получаем Deferred deferred = downloadPage(url) # Навешиваем на Deferred-объект обработчики успешных результатов # и ошибок (callback, errback). deferred.addCallback(printContents) deferred.addErrback(handleError)
На практике обычно мы возвращаем Deferred из функций, которые получают Deferred в процессе своей работы, навешиваем большое количество обработчиков, обрабатываем исключения, некоторые исключения возвращаем через Deferred (выбрасываем наверх). В качестве более сложного примера приведем код в асинхронном варианте для примера атомарного счетчика из статьи про структуры данных в memcached , здесь мы предполагаем, что доступ к memcached как сетевому сервису идет через Deferred, т.е. методы класса Memcache возвращают Deferred (который вернет либо результат операции, либо ошибку):

Class MCCounter(MemcacheObject): def __init__(self, mc, name): """ Конструктор. @param name: имя счетчика @type name: C{str} """ super(MCCounter, self).__init__(mc) self.key = "counter" + name def increment(self, value=1): """ Увеличить значение счетчика на указанное значение. @param value: инкремент @type value: C{int} @return: Deferred, результат операции """ def tryAdd(failure): # Обрабатываем только KeyError, всё остальное "вывалится" # ниже failure.trap(KeyError) # Пытаемся создать ключ, если раз его еще нет d = self.mc.add(self.key, value, 0) # Если вдруг кто-то еще создаст ключ раньше нас, # мы это обработаем d.addErrback(tryIncr) # Возвращаем Deferred, он "вклеивается" в цепочку # Deferred, в контексте которого мы выполняемся return d def tryIncr(failure): # Всё аналогично функции tryAdd failure.trap(KeyError) d = self.mc.incr(self.key, value) d.addErrback(tryAdd) return d # Пытаемся выполнить инкремент, получаем Deferred d = self.mc.incr(self.key, value) # Обрабатываем ошибку d.addErrback(tryAdd) # Возвращаем Deferred вызывающему коду, он может тем самым: # а) узнать, когда операция действительно завершится # б) обработать необработанные нами ошибки (например, разрыв соединения) return d def value(self): """ Получить значение счетчика. @return: текущее значение счетчика @rtype: C{int} @return: Deferred, значение счетчика """ def handleKeyError(failure): # Обрабатываем только KeyError failure.trap(KeyError) # Ключа нет - возвращаем 0, он станет результатом # вышележащего Deferred return 0 # Пытаемся получить значение ключа d = self.mc.get(self.key) # Будем обрабатывать ошибку отсутствия ключа d.addErrback(handleKeyError) # Возвращаем Deferred, наверх там можно будет повеситься # на его callback и получить искомое значение счетчика return d
Приведенный выше код можно написать “короче”, объединяя часто используемые операции, например:

Return self.mc.get(self.key).addErrback(handleKeyError)
Практически для каждой конструкции синхронного кода можно найти аналог в асинхронной концепции с Deferred:

  • последовательности синхронных операторов соответствует цепочка callback с асинхронными вызовами;
  • вызову одной подпрграммы с вводом-выводом из другой соответствует возврат Deferred из Deferred (ветвление Deferred);
  • глубокой цепочки вложенности, распространению исключений по стеку соответствует цепочка функций, возвращающие друг другу Deferred;
  • блокам try..except соответствуют обработчики ошибок (errback), которые могут “пробрасывать” исключения дальше, любое исключение в callback переводит выполнение в errback;
  • для “параллельного” выполнения асинхронных операций есть DeferredList .
Нити часто применяются в асинхронных программах для осуществления вычислительных процедур, осуществления блокирующегося ввода-вывода (когда не существует асинхронного аналога). Всё это легко моделируется в простой модели ‘worker’, тогда нет необходимости при грамотной архитектуре в явной синхронизации, при этом всё элегантно включается в общий поток вычислений с помощью Deferred:

Def doCalculation(a, b): """ В этой функции осуществляются вычисления, синхронные операции ввода-вывода, не затрагивающие основной поток. """ return a/b def printResult(result): print result def handleDivisionByZero(failure): failure.trap(ZeroDivisionError) print "Ooops! Division by zero!" deferToThread(doCalculation, 3, 2).addCallback(printResult).addCallback(lambda _: deferToThread(doCalculation, 3, 0).addErrback(handleDivisionByZero))
В приведенном выше примере функция deferToThread переносит выполнение указанной функции в отдельную нить и возвращает Deferred, через который будет асинхронно получен результат выполнения функции или исключение, если они будет выброшено. Первое деление (3/2) выполняется в отдельной нити, затем распечатывается его результат на экран, а затем запускается еще одно вычисление (3/0), которое генерирует исключение, обрабатываемое функцией handleDivisionByZero .

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

С некоторых пор я получаю много вопросов об асинхронном программировании. И, поняв, что данная тема интересует многих моих читателей, я решил написать статью, для объяснения этих терминов, тем более, что асинхронное программирование является очень важной частью современного Интернета.

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

Модель синхронного программирования.

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

Однопоточная среда - Single Threaded - если у нас есть пара задач, которые необходимо выполнить, а текущая система предоставляет только один поток, тогда задачи назначаются потоку одна за другой. Наглядно это можно изобразить вот так:

Где Thread 1 - один поток, Task 1 и Task 2, Task 3, Task 4 – соответствующие задачи.

Мы видим, что у нас есть поток (Thread 1 ) и четыре задачи , которые нужно выполнить. Поток начинает работу над задачами и завершает все задачи одну за другой.

Многопоточная среда - Multi-Threaded - в этой среде мы используем несколько потоков, которые могут выполнять эти задачи одновременно. Это означает, что у нас есть пул потоков (новые потоки также могут создаваться по необходимости на основе доступных ресурсов) и множество задач.

Мы видим, что у нас есть четыре потока и столько же задач. Поэтому каждый поток выполняет одну задачу и завершает ее. Это идеальный сценарий, но в обычных условиях у нас, как правило, больше задач, чем количество доступных потоков. И поэтому, когда один поток закончит выполнять некоторую задачу, он немедленно приступит к выполнению другой. Обратите внимание также и на тот факт, что новый поток создается не каждый раз, потому что ему нужны системные ресурсы, такие как такты процессора и память, которых может оказаться недостаточно.

Теперь давайте поговорим об асинхронной модели и о том, как она ведет себя в однопоточной и многопоточной среде.

Модель асинхронного программирования.

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

мы видим, что один поток отвечает за выполнение всех задач, чередуя их, друг с другом.

Если наша система способна создавать несколько потоков, то все потоки могут работать по асинхронной модели.

Мы видим, что те же задачи T4, T5, T6 обрабатываются несколькими потоками. В этом и состоит красота и сила этого сценария. Как вы можете видеть, задача T4 была запущена первой в потоке Thread 1 и завершена в потоке Thread 2 . Точно так же T6 завершается в Thread 2, Thread 3 и Thread 4 .

Итак, всего у нас четыре сценария –

  • Синхронный однопоточный
  • Синхронный многопоточный
  • Асинхронный однопоточный
  • Асинхронный многопоточный

Преимущества асинхронного программирования

Для любого приложения важны две вещи: удобство использования и производительность. Удобство использования важно потому, что когда пользователь нажимает кнопку, чтобы сохранить некоторые данные, это требует выполнения нескольких небольших задач, таких как чтение и заполнение данных во внутреннем объекте, установление соединения с SQL сервером и сохранение запроса там и. т. д.

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

Эффективность приложения также очень важна. Подсчитано, что при выполнении запроса около 70-80% времени теряется в ожидании зависимых задач. Поэтому, это место где асинхронное программирование как нельзя лучше придется кстати.

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

Последнее обновление: 31.10.2015

В прошлых темах было рассмотрено применение асинхронности с использованием ключевым слов async и await. Но кроме подобной модели использования асинхронных вызовов в C# имеется и другая модель - использование асинхронных делегатов. Асинхронные делегаты широко использовались до появления в C# async и await, сейчас же async и await существенно упрощают написание асинхронного кода. Тем не менее асинхронные делегаты по прежнему могут применяться. Поэтому рассмотрим их.

Асинхронные делегаты позволяют вызывать методы, на которые эти делегаты указывают, в асинхронном режиме. В теме про делегаты говорилось, что делегаты могут вызываться как с помощью метода Invoke , так и в асинхронном режиме с помощью пары методов BeginInvoke/EndInvoke . Рассмотрим на примере. Вначале посмотрим, что будет, если мы будем использовать обычный синхронный код в нашем приложении:

Using System; using System.Threading; namespace AsyncApp { class Program { public delegate int DisplayHandler(); static void Main(string args) { DisplayHandler handler = new DisplayHandler(Display); int result = handler.Invoke(); Console.WriteLine("Продолжается работа метода Main"); Console.WriteLine("Результат равен {0}", result); Console.ReadLine(); } static int Display() { Console.WriteLine("Начинается работа метода Display...."); int result = 0; for (int i = 1; i < 10; i++) { result += i * i; } Thread.Sleep(3000); Console.WriteLine("Завершается работа метода Display...."); return result; } } }

Здесь создается специальный делегат DisplayHandler, который в качестве ссылки принимает метод без параметров, который возвращает число. В данном случае таким методом является метод Display, который выполняет какую-то работу. В этом случае мы получим примерно следующий вывод:

Начинается работа метода Display.... Завершается работа метода Display.... Продолжается работа метода Main Результат равен 285

В общем-то можно было и не использовать делегат и напрямую вызвать метод Display. Но в любом случае после его вызова дальше блокируется работа метода Main, пока не завершится выполнение метода Display.

Теперь изменим пример с применением асинхронных вызовов делегата:

Using System; using System.Threading; namespace AsyncApp { class Program { public delegate int DisplayHandler(); static void Main(string args) { DisplayHandler handler = new DisplayHandler(Display); IAsyncResult resultObj = handler.BeginInvoke(null, null); Console.WriteLine("Продолжается работа метода Main"); int result = handler.EndInvoke(resultObj); Console.WriteLine("Результат равен {0}", result); Console.ReadLine(); } static int Display() { Console.WriteLine("Начинается работа метода Display...."); int result = 0; for (int i = 1; i < 10; i++) { result += i * i; } Thread.Sleep(3000); Console.WriteLine("Завершается работа метода Display...."); return result; } } }

Суть действий практически не изменилась, тот же метод Display, только теперь он вызывается в асинхронном режиме с помощью методов BedinInvoke/EndInvoke . И теперь мы можем получить немного другой вывод:

Начинается работа метода Display.... Продолжается работа метода Main Завершается работа метода Display.... Результат равен 285

Таким образом, после вызова метода Display через выражение handler.BeginInvoke(null, null) работа метода Main не приостанавливается. А выполнение метода Display через делегат DisplayHandler происходит в другом потоке. И лишь когда выполнение в методе Main дойдет до строки int result = handler.EndInvoke(resultObj); он блокируется и ожидает завершения выполнения метода Display.

Теперь рассмотрим особенности использования методов BeginInvoke и EndInvoke и интерфейса IAsyncResult.

Последнее обновление: 17.10.2018

Асинхронность позволяет вынести отдельные задачи из основного потока в специальные асинхронные методы или блоки кода. Особенно это актуально в графических программах, где продолжительные задачи могу блокировать интерфейс пользователя. И чтобы этого не произошло, нужно задействовать асинхронность. Также асинхронность несет выгоды в веб-приложениях при обработке запросов от пользователей, при обращении к базам данных или сетевым ресурсам. При больших запросах к базе данных асинхронный метод просто уснет на время, пока не получит данные от БД, а основной поток сможет продолжить свою работу. В синхронном же приложении, если бы код получения данных находился в основном потоке, этот поток просто бы блокировался на время получения данных.

Ключевыми для работы с асинхронными вызовами в C# являются два ключевых слова: async и await , цель которых - упростить написание асинхронного кода. Они используются вместе для создания асинхронного метода.

Асинхонный метод обладает следующими признаками:

    В заголовке метода используется модификатор async

    Метод содержит одно или несколько выражений await

    В качестве возвращаемого типа используется один из следующих:

    • ValueTask

Асинхронный метод, как и обычный, может использовать любое количество параметров или не использовать их вообще. Однако асинхронный метод не может определеять параметры с модификаторами out и ref .

Также стоит отметить, что слово async , которое указывается в определении метода, не делает автоматически метод асинхронным. Оно лишь указывает, что данный метод может содержать одно или несколько выражений await .

Рассмотрим пример асинхронного метода:

Using System; using System.Threading; using System.Threading.Tasks; namespace HelloApp { class Program { static void Factorial() { int result = 1; for(int i = 1; i <= 6; i++) { result *= i; } Thread.Sleep(8000); Console.WriteLine($"Факториал равен {result}"); } // определение асинхронного метода static async void FactorialAsync() { Console.WriteLine("Начало метода FactorialAsync"); // выполняется синхронно await Task.Run(()=>Factorial()); // выполняется асинхронно Console.WriteLine("Конец метода FactorialAsync"); } static void Main(string args) { FactorialAsync(); // вызов асинхронного метода Console.WriteLine("Введите число: "); int n = Int32.Parse(Console.ReadLine()); Console.WriteLine($"Квадрат числа равен {n * n}"); Console.Read(); } } }

Здесь прежде всего определен обычный метод подсчета факториала. Для имитации долгой работы в нем используется задержка на 8 секунд с помощью метода Thread.Sleep() . Условно это некоторый метод, который выполняет некоторую работу продолжительное время. Но для упрощения понимания он просто подсчитывает факториал числа 6.

Также здесь определен асинхронный метод FactorialAsync() . Асинхронным он является потому, что имеет в определении перед возвращаемым типом модификатор async , его возвращаемым типом является void, и в теле метода определено выражение await .

Выражение await определяет задачу, которая будет выполняться асинхронно. В данном случае подобная задача представляет выполнение функции факториала:

Await Task.Run(()=>Factorial());

По негласным правилам в названии асинхроннных методов принято использовать суффикс Async - FactorialAsync () , хотя в принципе это необязательно делать.

Сам факториал мы получаем в асинхронном методе FactorialAsync . Асинхронным он является потому, что он объявлен с модификатором async и содержит использование ключевого слова await .

И в методе Main мы вызываем этот асинхронный метод.

Посмотрим, какой у программы будет консольный вывод:

Начало метода FactorialAsync Введите число: 7 Квадрат числа равен 49 Конец метода Main Факториал равен 720 Окончание метода FactorialAsync

Разберем поэтапно, что здесь происходит:

    Запускается метод Main, в котором вызывается асинхронный метод FactorialAsync.

    Метод FactorialAsync начинает выполняться синхронно вплоть до выражения await.

    Выражение await запускает асинхронную задачу Task.Run(()=>Factorial())

    Пока выполняется асинхронная задача Task.Run(()=>Factorial()) (а она может выполняться довольно продожительное время), выполнение кода возвращается в вызывающий метод - то есть в метод Main. В методе Main нам будет предложено ввести число для вычисления квадрата числа.

    В этом и преимущество асинхронных методов - асинхронная задача, которая может выполняться довольно долгое время, не блокирует метод Main, и мы можем продолжать работу с ним, например, вводить и обрабатывать данные.

    Когда асинхронная задача завешила свое выполнение (в случае выше - подсчитала факториал числа), продолжает работу асинхронный метод FactorialAsync, который вызвал асинхронную задачу.

Функция факториала, возможно, представляет не самый показательный пример, так как в реальности в данном случае нет смысла делать ее асинхронной. Но рассмотрим другой пример - чтение-запись файла:

Using System; using System.Threading; using System.Threading.Tasks; using System.IO; namespace HelloApp { class Program { static async void ReadWriteAsync() { string s = "Hello world! One step at a time"; // hello.txt - файл, который будет записываться и считываться using (StreamWriter writer = new StreamWriter("hello.txt", false)) { await writer.WriteLineAsync(s); // асинхронная запись в файл } using (StreamReader reader = new StreamReader("hello.txt")) { string result = await reader.ReadToEndAsync(); // асинхронное чтение из файла Console.WriteLine(result); } } static void Main(string args) { ReadWriteAsync(); Console.WriteLine("Некоторая работа"); Console.Read(); } } }

Асинхронный метод ReadWriteAsync() выполнение запись в файл некоторой строки и затем считывает записанный файл. Подобные операции могут занимать продолжительное время, особенно при больших объемах данных, поэтому такие операции лучше делать асинхронными.

Фреймворк.NET уже имеет встроенную поддержку таких операций. Например, в классе StreamWriter определен метод WriteLineAsync() . По сути он уже представляет асинхронную операцию и принимает в качестве параметра некоторую строку, которую надо записать в файл. Поскольку этот метод представляет асинхронную операцию, то вызов этого метода мы можем оформить в выражение await :

Await writer.WriteLineAsync(s); // асинхронная запись в файл

Аналогично в классе StreamReader определен метод ReadToEndAsync() , который также представляет асинхронную операцию и который возвращает весь считанный текст.

Во фреймворке.NET Core определено много подобных методов. Как правило, они связаны с работой с файлами, отправкой сетевых запросов или запросов к базе данных. Их легко узнать по суффиксу Async . То есть если метод имеет подобный суффикс в названии, то с большей степенью вроятности его можно использовать в выражении await.

Static void Main(string args) { ReadWriteAsync(); Console.WriteLine("Некоторая работа"); Console.Read(); }

И опять же когда выполнение в методе ReadWriteAsync доходит до первого выражения await, управление возвращается в метод Main, и мы можем продолжать с ним работу. Запись в файл и считывания файла будут производиться параллельно и не будут блокировать работу метода Main.

Определение асинхронной операции

Как, выше уже было сказано, фреймворк.NET Core имеет много встроенных методов, которые представляют асинхронную операцию. Они заканчиваются на суффикс Async. И перед вызывами подобных методов мы можем указывать оператор await . Например:

StreamWriter writer = new StreamWriter("hello.txt", false); await writer.WriteLineAsync("Hello"); // асинхронная запись в файл

Либо мы сами можем определить асинхронную операцию, используя метод Task.Run() :

Static void Factorial() { int result = 1; for (int i = 1; i <= 6; i++) { result *= i; } Thread.Sleep(8000); Console.WriteLine($"Факториал равен {result}"); } // определение асинхронного метода static async void FactorialAsync() { await Task.Run(()=>Factorial()); // вызов асинхронной операции }

Можно определить асинхронную операцию с помощью лямбда-выражения:

Static async void FactorialAsync() { await Task.Run(() => { int result = 1; for (int i = 1; i <= 6; i++) { result *= i; } Thread.Sleep(8000); Console.WriteLine($"Факториал равен {result}"); }); }

Передача параметров в асинхронную операцию

Выше вычислялся факториал 6, но, допустим, мы хотим вычислять факториалы разных чисел:

Using System; using System.Threading; using System.Threading.Tasks; namespace HelloApp { class Program { static void Factorial(int n) { int result = 1; for (int i = 1; i <= n; i++) { result *= i; } Thread.Sleep(5000); Console.WriteLine($"Факториал равен {result}"); } // определение асинхронного метода static async void FactorialAsync(int n) { await Task.Run(()=>Factorial(n)); } static void Main(string args) { FactorialAsync(5); FactorialAsync(6); Console.WriteLine("Некоторая работа"); Console.Read(); } } }

Получение результата из асинхронной операции

Асинхронная операция может возвращать некоторый результат, получить который мы можем так же, как и при вызове обычного метода:

Using System; using System.Threading; using System.Threading.Tasks; namespace HelloApp { class Program { static int Factorial(int n) { int result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; } // определение асинхронного метода static async void FactorialAsync(int n) { int x = await Task.Run(()=>Factorial(n)); Console.WriteLine($"Факториал равен {x}"); } static void Main(string args) { FactorialAsync(5); FactorialAsync(6); Console.Read(); } } }

Метод Factorial возвращает значение типа int, это значение мы можем получить, просто присвоив результат асинхронной операции переменной данного типа: int x = await Task.Run(()=>Factorial(n));

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

Есть несколько разных понятий, связанных с областью параллельных вычислений.

  • Конкурентное исполнение (concurrency)
  • Параллельное исполнение (parallel execution)
  • Многопоточное исполнение (multithreading)
  • Асинхронное исполнение (asynchrony)

Каждый из этих терминов строго определен и имеет четкое значение.

Конкурентность (concurrency)

Конкурентность (*) (concurrency) - это наиболее общий термин, который говорит, что одновременно выполняется более одной задачи. Например, вы можете одновременно смотреть телевизор и комментить фоточки в фейсбуке. Винда, даже 95-я могла (**) одновременно играть музыку и показывать фотки.

(*) К сожалению, вменяемого русскоязычного термина я не знаю. Википедия говорит, что concurrent computing - это параллельные вычисления, но как тогда будет parallel computing по русски?

(**) Да, вспоминается анекдот про Билла Гейтса и многозадачность винды, но, теоретически винда могла делать несколько дел одновременно. Хотя и не любых.

Конкурентное исполнение - это самый общий термин, который не говорит о том, каким образом эта конкурентность будет получена: путем приостановки некоторых вычислительных элементов и их переключение на другую задачу, путем действительно одновременного исполнения, путем делегации работы другим устройствам или еще как-то. Это не важно.

Конкурентное исполнение говорит о том, что за определенный промежуток времени будет решена более, чем одна задача. Точка.

Параллельное исполнение

Параллельное исполнение (parallel computing) подразумевает наличие более одного вычислительного устройства (например, процессора), которые будут одновременно выполнять несколько задач.

Параллельное исполнение - это строгое подмножество конкурентного исполнения. Это значит, что на компьютере с одним процессором параллельное программирование - невозможно;)

Многопоточность

Многопоточность - это один из способов реализации конкурентного исполнения путем выделения абстракции "рабочего потока" (worker thread).

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

Асинхронное исполнение

Асинхронность (asynchrony) подразумевает, что операция может быть выполнена кем-то на стороне: удаленным веб-узлом, сервером или другим устройством за пределами текущего вычислительного устройства.

Основное свойство таких операций в том, что начало такой операции требует значительно меньшего времени, чем основная работа. Что позволяет выполнять множество асинхронных операций одновременно даже на устройстве с небольшим числом вычислительных устройств.

CPU-bound и IO-Bound операции

Еще один важный момент, с точки зрения разработчика - разница между CPU-bound и IO-bound операциями. CPU-Bound операции нагружают вычислительные мощности текущего устройства, а IO-Bound позволяют выполнить задачу вне текущей железки.

Разница важна тем, что число одновременных операций зависит от того, к какой категории они относятся. Вполне нормально запустить параллельно сотни IO-Bound операций, и надеяться, что хватит ресурсов обработать все результаты. Запускать же параллельно слишком большое число CPU-bound операций (больше, чем число вычислительных устройств) бессмысленно.

Возвращаясь к исходному вопросу: нет смысла выполнять в 1000 потоков метод Calc , если он является CPU-Intensive (нагружает центральный процессор), поскольку это приведет к падению общей эффективности вычислений. ОС-ке придется переключать несколько доступных ядер для обслуживания сотен потоков. А этот процесс не является дешевым.

Самым простым и эффективным способом решения CPU-Intensive задачи, заключается в использовании идиомы Fork-Join: задачу (например, входные данные) нужно разбить на определенное число подзадач, которые можно выполнить параллельно. Каждая подзадача должна быть независимой и не обращаться к разделяемым переменным/памяти. Затем, нужно собрать промежуточные результаты и объединить их.

Выглядит это очень интересно:

IEnumerable yourData = GetYourData(); var result = yourData.AsParallel() // начинаем обрабатывать параллельно.Select(d => ComputeMD5(d)) // Вычисляем параллельно.Where(md5 => IsValid(md5)) .ToArray(); // Возврвщаемся к синхронной модели

В этом случае, число потоков будет контролироваться библиотечным кодом в недрах CLR/TPL и метод ComputeMD5 будет вызван параллельно N-раз на компьютере с N-процессорами (ядрами).