Синхронная асинхронность в C++
Наверняка все, кто изучал старый добрый стандарт C++11, знают о существовании в стандартной библиотеке вызова std::async, который позволяет выполнить некий код асинхронно (более точно – поведение указывается первым параметром вызова).
Согласно документации, вызов с параметром std::launch::async обещает выполнить пользовательский код в отдельном потоке. Посмотрим на приведённый ниже код.
#include #include #include int main(int argc, char* argv[]) < int count = 10; std::async(std::launch::async, [&count] < for(int i=0; i>); std::async(std::launch::async, [&count] < for(int i=0; i>); return 0; >В строках 8-13 запускаем асинхронное выполнение простой lambda-функции, которая должна вывести на экран цифру «1» каждую миллисекунду десять раз. В строках 14-19 запускаем выполнение аналогичной функции, но на этот раз она будет выводить на экран цифру «2». Что можно ожидать на экране по окончанию выполнения программы?
Кто сказал, что «результат не определён»?
Идея такой гипотезы заключается в том, что оба потока будут выполняться параллельно, поэтому вывод на экран перемешается. Мы можем увидеть на экране, например, такую последовательность:
12212121211212211221Звучит логично, но эта гипотеза неверна. На самом деле на экран гарантированно будет выведена последовательность:
11111111112222222222Почему? Что произошло?
А произошла принудительная синхронизация двух потоков. Выполнение второго потока (с выводом цифры «2») гарантированно начнётся только после того, как первый поток закончит своё выполнение.
Кто догадается, почему?
На самом деле не всё так просто. Но достаточно задуматься, про что мы забыли в этом примере? А забыли мы про то, что в качестве результата вызов std::async возвращает std::future. Если бы мы написали наш пример следующим образом, то результат на экране стал бы действительно неопределённым:
#include #include #include int main(int argc, char* argv[]) < int count = 10; auto future1 = std::async(std::launch::async, [&count] < for(int i=0; i>); auto future2 = std::async(std::launch::async, [&count] < for(int i=0; i>); return 0; >Вот теперь на экране действительно может быть любая последовательность из перемешанных двадцати цифр 1 и 2. Почему результат так кардинально изменился, стоило нам только лишь сохранить std::future, которое вернул вызов std::async?
Как говорится, всё законно, всё по стандарту
Стандарт гарантирует, что окончание выполнение потока, запущенного вызовом std::async, синхронизировано с вызовом получения результата std::future::get или с освобождением общего состояния (shared state) – области памяти, ответственной за передачу результата между std::async и std::future.
В первом примере автоматическое удаление временного объекта std::future, который был возвращён из первого вызова std::async, приводит к освобождению общего состояния и автоматической синхронизации двух потоков. Просто не сохранив результат вызова std::async, мы получили ожидание – второй поток не начнёт выполнение до окончания выполнения первого потока.
Есть вопрос? Напишите в комментариях!
Async/await
Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно прост для понимания и использования.
Асинхронные функции
Начнём с ключевого слова async . Оно ставится перед функцией, вот так:
async function f()У слова async один простой смысл: эта функция всегда возвращает промис. Значения других типов оборачиваются в завершившийся успешно промис автоматически.
Например, эта функция возвратит выполненный промис с результатом 1 :
async function f() < return 1; >f().then(alert); // 1
Можно и явно вернуть промис, результат будет одинаковым:
async function f() < return Promise.resolve(1); >f().then(alert); // 1
Так что ключевое слово async перед функцией гарантирует, что эта функция в любом случае вернёт промис. Согласитесь, достаточно просто? Но это ещё не всё. Есть другое ключевое слово – await , которое можно использовать только внутри async -функций.
Await
// работает только внутри async–функций let value = await promise;
Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.
В этом примере промис успешно выполнится через 1 секунду:
async function f() < let promise = new Promise((resolve, reject) => < setTimeout(() =>resolve("готово!"), 1000) >); let result = await promise; // будет ждать, пока промис не выполнится (*) alert(result); // "готово!" > f();
В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат выполнения промиса, и браузер отобразит alert-окно «готово!».
Обратите внимание, хотя await и заставляет JavaScript дожидаться выполнения промиса, это не отнимает ресурсов процессора. Пока промис не выполнится, JS-движок может заниматься другими задачами: выполнять прочие скрипты, обрабатывать события и т.п.
По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем promise.then .
await нельзя использовать в обычных функциях
Если мы попробуем использовать await внутри функции, объявленной без async , получим синтаксическую ошибку:
Aсинхронное программирование
Нередко программа выполняет такие операции, которые могут занять продолжительное время, например, обращение к сетевым ресурсам, чтение-запись файлов, обращение к базе данных и т.д. Такие операции могут серьезно нагрузить приложение. Особенно это актуально в графических (десктопных или мобильных) приложениях, где продолжительные операции могут блокировать интерфейс пользователя и негативно повлиять на желание пользователя работать с программой, или в веб-приложениях, которые должны быть готовы обслуживать тысячи запросов в секунду. В синхронном приложении при выполнении продолжительных операций в основном потоке этот поток просто бы блокировался на время выполнения операции. И чтобы продолжительные операции не блокировали общую работу приложения, в C# можно задействовать асинхронность.
Асинхронность позволяет вынести отдельные задачи из основного потока в специальные асинхронные методы и при этом более экономно использовать потоки. Асинхронные методы выполняются в отдельных потоках. Однако при выполнении продолжительной операции поток асинхронного метода возвратится в пул потоков и будет использоваться для других задач. А когда продолжительная операция завершит свое выполнение, для асинхронного метода опять выделяется поток из пула потоков, и асинхронный метод продолжает свою работу.
Ключевыми для работы с асинхронными вызовами в C# являются два оператора: async и await , цель которых - упростить написание асинхронного кода. Они используются вместе для создания асинхронного метода.
Асинхронный метод обладает следующими признаками:
- В заголовке метода используется модификатор async
- Метод содержит одно или несколько выражений await
- В качестве возвращаемого типа используется один из следующих:
- void
- Task
- Task
- ValueTask
Асинхронный метод, как и обычный, может использовать любое количество параметров или не использовать их вообще. Однако асинхронный метод не может определять параметры с модификаторами out , ref и in .
Также стоит отметить, что слово async , которое указывается в определении метода, НЕ делает автоматически метод асинхронным. Оно лишь указывает, что данный метод может содержать одно или несколько выражений await .
Рассмотрим простейший пример определения и вызова асинхронного метода:
await PrintAsync(); // вызов асинхронного метода Console.WriteLine("Некоторые действия в методе Main"); void Print() < Thread.Sleep(3000); // имитация продолжительной работы Console.WriteLine("Hello METANIT.COM"); >// определение асинхронного метода async Task PrintAsync() < Console.WriteLine("Начало метода PrintAsync"); // выполняется синхронно await Task.Run(() =>Print()); // выполняется асинхронно Console.WriteLine("Конец метода PrintAsync"); >
Здесь прежде всего определен обычный метод Print, который просто выводит некоторую строку на консоль. Для имитации долгой работы в нем используется задержка на 3 секунд с помощью метода Thread.Sleep() . То есть условно Print - это некоторый метод, который выполняет некоторую продолжительную операцию. В реальном приложении это могло бы быть обращение к базе данных или чтение-запись файлов, но для упрощения понимания он просто выводит строку на консоль.
Также здесь определен асинхронный метод PrintAsync() . Асинхронным он является потому, что имеет в определении перед возвращаемым типом модификатор async , его возвращаемым типом является Task, и в теле метода определено выражение await .
Стоит отметить, что явным образом метод PrintAsync не возвращает никакого объекта Task, однако поскольку в теле метода применяется выражение await , то в качестве возвращаемого типа можно использовать тип Task.
Оператор await предваряет выполнение задачи, которая будет выполняться асинхронно. В данном случае подобная операция представляет выполнение метода Print:
await Task.Run(()=>Print());
По негласным правилам в названии асинхроннных методов принято использовать суффикс Async - Print Async () , хотя в принципе это необязательно делать.
И затем в программе (в данном случае в методе Main) вызывается этот асинхронный метод.
await PrintAsync(); // вызов асинхронного метода
Посмотрим, какой у программы будет консольный вывод:
Начало метода PrintAsync Hello METANIT.COM Конец метода PrintAsync Некоторые действия в методе Main
Разберем поэтапно, что здесь происходит:
- Запускается программа, а точнее метод Main, в котором вызывается асинхронный метод PrintAsync.
- Метод PrintAsync начинает выполняться синхронно вплоть до выражения await.
Console.WriteLine("Начало метода PrintAsync"); // выполняется синхронно
Асинхронный метод Main
Стоит учитывать, что оператор await можно применять только в методе, который имеет модификатор async . И если мы в методе Main используем оператор await , то метод Main тоже должен быть определен как асинхронный. То есть предыдущий пример фактически будет аналогичен следующему:
class Program < async static Task Main(string[] args) < await PrintAsync(); // вызов асинхронного метода Console.WriteLine("Некоторые действия в методе Main"); void Print() < Thread.Sleep(3000); // имитация продолжительной работы Console.WriteLine("Hello METANIT.COM"); >// определение асинхронного метода async Task PrintAsync() < Console.WriteLine("Начало метода PrintAsync"); // выполняется синхронно await Task.Run(() =>Print()); // выполняется асинхронно Console.WriteLine("Конец метода PrintAsync"); > > >
Задержка асинхронной операции и Task.Delay
В асинхронных методах для остановки метода на некоторое время можно применять метод Task.Delay() . В качестве параметра он принимает количество миллисекунд в виде значения int, либо объект TimeSpan, который задает время задержки:
await PrintAsync(); // вызов асинхронного метода Console.WriteLine("Некоторые действия в методе Main"); // определение асинхронного метода async Task PrintAsync() < await Task.Delay(3000); // имитация продолжительной работы // или так //await Task.Delay(TimeSpan.FromMilliseconds(3000)); Console.WriteLine("Hello METANIT.COM"); >
Причем метод Task.Delay сам по себе представляет асинхронную операцию, поэтому к нему применяется оператор await.
Преимущества асинхронности
Выше приведенные примеры являются упрощением, и вряд ли их можно считать показательным. Рассмотрим другой пример:
PrintName("Tom"); PrintName("Bob"); PrintName("Sam"); void PrintName(string name) < Thread.Sleep(3000); // имитация продолжительной работы Console.WriteLine(name); >
Данный код является синхронным и выполняет последовательно три вызова метода PrintName. Поскольку для имитации продолжительной работы в методе установлена задержка на три секунды, то общее выполнение программы займет не менее 9 секунд. Так как каждый последующий вызов PrintName будет ждать пока завершится предыдущий.
Изменим в программе синхронный метод PrintName на асинхронный:
await PrintNameAsync("Tom"); await PrintNameAsync("Bob"); await PrintNameAsync("Sam"); // определение асинхронного метода async Task PrintNameAsync(string name) < await Task.Delay(3000); // имитация продолжительной работы Console.WriteLine(name); >
Вместо метода PrintName теперь вызывается три раза PrintNameAsync. Для имитации продолжительной работы в методе установлена задержка на 3 секунды с помощью вызова Task.Delay(3000) . И поскольку при вызовае каждого метода применяется оператор await, который останавливает выполнение до завершения асинхронного метода, то общее выполнение программы опять же займет не менее 9 секунд. Тем не менее теперь выполнение асинхронных операций не блокирует основной поток.
Теперь оптимизируем программу:
var tomTask = PrintNameAsync("Tom"); var bobTask = PrintNameAsync("Bob"); var samTask = PrintNameAsync("Sam"); await tomTask; await bobTask; await samTask; // определение асинхронного метода async Task PrintNameAsync(string name) < await Task.Delay(3000); // имитация продолжительной работы Console.WriteLine(name); >
В данном случае задачи фактически запускаются при определении. А оператор await применяется лишь тогда, когда нам нужно дождаться завершения асинхронных операций - то есть в конце программы. И в этом случае общее выполнение программы займет не менее 3 секунд, но гораздо меньше 9 секунд.
Определение асинхронного лямбда-выражения
Асинхронную операцию можно определить не только с помощью отдельного метода, но и с помощью лямбда-выражения:
// асинхронное лямбда-выражение Func printer = async (message) => < await Task.Delay(1000); Console.WriteLine(message); >; await printer("Hello World"); await printer("Hello METANIT.COM");
Что такое асинхронная функция/метод в C# и программировании в целом?
Что именно стоит называть асинхронным методом? Какой критерий должен быть что бы назвать метод асинхронным? Другая терминология вроде понятна, но давайте повторим ее что бы дальше использовать в попытках опеределить понятие асинхронного метода.
Вспоминаем связанные понятия
Есть понятие Asynchronous method invocation (AMI) -
call site is not blocked while waiting for the called code to finish. Instead, the calling thread is notified when the reply arrives.
причем слово invocation могут менять на call или execution. Есть понятие асинхронность, но в программировании похоже его используют с AMI почти взаимозаменяемо (1) -
выполнение операции без ожидания окончания завершения этой обработки, результат же выполнения может быть обработан позднее.
Часто приводят пример с чайником который ставят кипятить, для того что бы обьяснить что это такое.
Давайте искать определение для асинхронного метода, и на время забудем то как мы себе это представляли для того что бы быть обьективными.
- Источник первый - документация к ключевому слову async -
Use the async modifier to specify that a method, lambda expression, or anonymous method is asynchronous. If you use this modifier on a method or expression, it's referred to as an async method.
Что можно перевести как: Если вы используете этот модификатор в методе или выражении, он называется асинхронным методом. Понятно что не каждый метод с async обязательно в реальности содержит AMI или что-то связанное с асинхронностью.
- С другой стороны есть такой источник 2:
An asynchronous method is one that we call to start the lengthy operation. The method should do what it needs to start the operation and return "very quickly" so that there are no processing delays.
A method is asynchronous if your thread tells the method that it needs the work to be done, and the method says "OK, I'll do that and I'll call you when it is finished"
Это намекает нам на то что в опеделении асинхронного метода должно быть что то про AMI, который мы вспоминали выше.
- Еще у того же микрософта в старой доке (до появления ключевого слова async) можно встретить следующее использование этого термина (3):
If the asynchronous method experiences an unhandled exception.
Значит вроде как такое понятие было еще до ключевого слова async, но возможно с его появление появились новые смыслы связанные чисто с синтаксисом языка.
К сожалению изучать источники дело долгое, поэтому даю возможность дополнить тем кто будет отвечать на вопрос.
Пробуем по источникам дать определение
Какие варианты определения асинхронного метода приходят в голову исходя из изученных источников:
- это метод с ключевым словом async при обьявлении. Пример:
async void SimpleAsync() <>
- это метод, который можно использовать для не блокирующего вызова. Пример:
async void DelayAsync() < Task.Delay(1); >//Task.Delay асинхронный, так его можно использовать для AMI. Прям тут и используют. async void Delay2Async() < await Task.Delay(1); >//Task.Delay асинхронный, так его можно использовать для AMI. Прям тут и используют. async Task Delay3Async() < await Task.Delay(1); >//Task.Delay асинхронный, так его можно использовать для AMI. Прям тут и используют. async void DelayExternalAsync() < DelayAsync(); Delay2Async(); >//DelayAsync и Delay2Async асинхронные, так их можно использовать для AMI. Прям тут и используют. async void DelayExternalAwaitAsync() < await Delay3Async(); >//Delay3Async асинхронный, так его можно использовать для AMI. Прям тут и используют. //DelayExternalAsync и DelayExternalAwaitAsync асинхронные, так их можно использовать для AMI. Хотя тут их не используют для этого. async Task JustEmptyAsync() < return await Task.FromResult(1); >// JustEmptyAsync не асинхронный, так его не можно использовать для AMI в других местах, так как внутри ничего асинхронного.
- это метод в котором происходит не блокирующий вызов. В примере почти все методы асинхронные, но не Task.Delay, так как в нем самом AMI нет, по крайней мере смотря на имеющийся код (может это и не так, но можем на время сделать такое допущение. Хотя тут тоже вопрос - существуют ли такие методы, которые могут быть вызваны не блокирующе, но сами не содержат AMI? Если да, то именно такой метод и хотелось бы тут использовать для примера, иначе этот вариант похоже эквивалентен второму):
async void DelayAsync() < Task.Delay(1); >//DelayAsync асинхронный, так как в нем есть AMI async void Delay2Async() < await Task.Delay(1); >//Delay2Async асинхронный, так как в нем есть AMI async Task Delay3Async() < await Task.Delay(1); >//Delay3Async асинхронный, так как в нем есть AMI async void DelayExternalAsync() < DelayAsync(); >//DelayExternalAsync асинхронный, так как в нем есть AMI async void DelayExternalAwaitAsync() < await Delay3Async(); >//DelayExternalAwaitAsync асинхронный, так как в нем есть AMI
- Другие варианты.
Подчеркнем первоначальный вопрос и дополним его тем что узнали в ходе изучения источников
Какой первоисточник этого понятия? Какое значение оно имеет сейчас? Или это как и многие другие термины в которые на слуху но четкого определения не имеют - кто как хочет так и понимает? Есть несколько определений, которые накопились со временем?