Как стать автором
Обновить

Комментарии 27

Спасибо автору. Я читал множество статей на эту тему async/await и эта самая полная и подробная.
Хотел бы ещё чтобы упомянули про вызов асинхронного кода из синхронного. И почему нельзя использовать Task.Result (или можно) и dealock которым можно получить.
Также хотелось бы упомянуть использование асинхронного кода без async/await с возвращение Task из других методов.
Как-то довелось наблюдать код веб-приложения, где каждый асинхронный вызов был украшен сие ускорителем. Это не имеет никакого эффекта, кроме визуального отвращения. Стандартное веб-приложение ASP.NET Core не имеет каких-то уникальных контекстов (если вы их сами не напишете, конечно).

Два чая этому господину! Приходилось такое видеть.
HttpContext
>> При этом если метод содержит await, обязательно его помечать async, обратное неверно, но бессмысленно
Это имеет смысл. Например, для методов, которые ничего не возвращают (void).
Видимо, вы не так поняли. Имелось в виду, что помечать метод async, если в нем нет await не имеет смысла. Метод преобразуется в заглушку, будет сгенерирована стейт-машина, но это будет сделано зря, т.к. нигде не будет присоединено продолжений. Компилятор при этом выдаст предупреждение «В данном асинхронном методе отсутствуют операторы await ...».
>> Имелось в виду, что помечать метод async, если в нем нет await не имеет смысла.
Я всё правильно понял. Есть методы, которые ничего не возвращают в принципе. Но при этом у разработчика может быть желание сделать данный метод асинхронным.
Возвращаемое значение не влияет на асинхронность. Если есть метод, возвращающий void, то его асинхронность зависит от наличия внутри вызовов асинхронных операций с применением await для ожидания их завершения.
Спасибо за статтю)
Если асинхронная операция к этому моменту завершена, то выполнение продолжается синхронно, в том же потоке.

А как она может быть уже завершенной? Правильно я понял, что эта задача попала в одну из очередей и могла быть оттуда подхвачена потоком для выполнения?
Пример данного поведения приведен в главе с машиной состояний и ее кодом. Далее следует маленький пример с Thread.Sleep. Там мы запускаем задачу, которая выполняется секунду, далее засыпает на 1.7 секунды и ожидаем запущенную задачу. Т.к. мы спали достаточно долго и запущенная задача уже выполнилась, то нет смысла присоединять продолжения и т.д. Мы просто продолжаем выполнение, т.к. результат к этому моменту доступен. Если вы измените засыпание с 1.7 на что-нибудь меньше секунды (время за которое выполняется задача), то продолжение выполнится в другом потоке, т.е. на консоль будут выведены разные числа.
В реальном мире так обычно бывает, когда операция выполняется в ряде случаев сразу, а в остальных уже дольше. Пример тому файлы (которые я описывал) — если мы записываем достаточно малый (влезающий в буфер) объем данных — то продолжение никуда не присоединяется, т.к. операция завершается синхронно. Если мы пишем большой объем данных, то операция выполняется асинхронно и продолжение будет присоединено к еще не завершенной задаче. Вот еще вам пример с файлами
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
int bufferSize = 4096; // менять здесь. Можно менять или размер буфера, или данных.
int dataSize = 2000; //Если bufferSize > dataSize, то продолжение выполняется синхронно в том же потоке. В противном случае - в другом
using (var fs = new FileStream("aswt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, bufferSize, true))
{
        var ar = new byte[dataSize];
        new Random().NextBytes(ar);
        await fs.WriteAsync(ar);
}
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Спасибо за ответ)
Там мы запускаем задачу, которая выполняется секунду

Мой вопрос был о жизненном чикл этой задачи. Мы запустили эту задачу из потока Х и сказали чтобы поток Х спал (ничего не делал) 1.7 секунды. Кто тогда выполнил эту задачу? Опишите, пожалуйста, последовательность действий, которые происходят от старта этой задачи до ее окончания.
Зависит от конкретной задачи. В ряде случаев внутри вызванной вами асинхронной операции работа будет отправлена на выполнение в пул потоков, т.е. над ней будет работать отдельный поток. В случаях с операциями ввода-вывода действия по чтению-записи могут быть делегированы на контроллер устройства, не занимая лишних потоков.
Поток выполняется как обычно, далее вызывает асинхронную операцию. Она начинает выполняться одним из вышеперечисленных способов. В вызвавший ее поток возвращается Task — индикатор завершенности этой операции. Далее поток просто засыпает на 1.7с. после того, как он просыпается он встречает await и проверяет, завершена ли операция, представленная тем Task. В данном случае да, завершена. Поэтому мы можем продолжать выполнение в этом же потоке как обычно.
При работе с асинхронностью мы оперируем задачами. Это совсем не то же самое, что поток. Одна задача может выполняться многими потоками, а один поток может выполнять много задач.

Вы не могли бы немного пpояснить вашу терминологию? То есть я правильно понимаю что под "потоком" в вашей статье подразумевается thread? И если да, то как абстрактное понятие или конкретно "System.Threading.Thread"?

Под задаче подразумевал те действия, состояние которых отражено в объекте Task. Как-то так. Поток — просто поток выполнения, насколько я помню, нигде не опирался на конкретные детали System.Threading.Thread.
Если есть вопросы конкретно по материалу, задавайте. Мне тогда будет легче пояснить

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


Ну а вообще я сейчас "медитирую" вот над этим пунктом:


Одна задача может выполняться многими потоками

И пытаюсь понять как это конкретно выглядит и какие последствия может иметь. Ну или для начала: скажем у меня "blackbox" с двумя асинхронными методами и я точно не знаю что там и как в этих методах выполняется. И я эти два метода вызываю друг за другом. То есть как-то вот так:


var x = await BlackBox.Method1Async();
var y = await BlackBox.Method2Async();


Может ли такое случится что какая-то часть Method2 в реальном времени выполнится раньше чем выполнится весь Method1. По идее вроде бы не должно, но 100% уверенности у меня нет :)

Ну в теории да, если кто-то пишет странные методы вроде этого (не дожидаясь запущенных задач).
    public static async Task Main()
    {
        await BlackBox.Method1Async();
        await BlackBox.Method2Async();
        Thread.Sleep(3000);
    }

    public static class BlackBox
    {
        public static async Task Method1Async()
        {
            Console.WriteLine($"Method1Async 1");
            await Task.Delay(500);
            Console.WriteLine($"Method1Async 2");
            Task.Run(async () => // не дождались
            {
                await Task.Delay(500);
                Console.WriteLine($"Method1Async 3");
            });
        }

        public static async Task Method2Async()
        {
            Console.WriteLine($"Method2Async 1");
            await Task.Delay(500);
            Console.WriteLine($"Method2Async 2");
        }
    }

Но в нормальных ситуациях — нет, во всяком случае мне на ум ничего не приходит.
Одна задача многими потоками: обычный асинхронный метод, который выполняется реально асинхронно. Его продолжение может быть выполнено на потоке, отличном от первоначального. В то же время код, который вызывает этот метод не думает об этом. С его стороны — это логически едина язадача, над которой поработало несколько потоков. Примерно это я имел в виду
Одна задача многими потоками: обычный асинхронный метод, который выполняется реально асинхронно. Его продолжение может быть выполнено на потоке, отличном от первоначального.

Спасибо, теперь стало понятнее.

Насчет "Task.Factory.StartNew(action, TaskCreationOptions.LongRunning)" — бессмысленно, если внутри задачи будут использоваться await.

Ну кроме редких случаев да, вы правы. После первого же await уже выполнение будет возложено на пул. Разве что вы долго и упорно что-то делаете и в конце асинхронно, допустим, отсылаете результаты по сети. Тогда смысл есть.
Объект ожидания должен реализовать интерфейс INotifyCompletion, который обязывает реализовать метод void OnCompleted(Action continuation). Также он должен иметь экземплярные свойство bool IsCompleted, метод void GetResult(). Может быть как структурой, так и классом.

Метод GetResult () не обязательно должен возвращать void. Это может быть любой другой тип. Для примера мы можем возвращать квадрат числа над которым выполняем await:
    class Program
    {
        static async Task Main(string[] args)
        {
            var pow = await 3;
            Console.WriteLine(pow);
        }       
    }

    public static class WeirdExtensions
    {
        public static AnyTypeAwaiter GetAwaiter(this int number) 
             => new AnyTypeAwaiter(number);

        public class AnyTypeAwaiter : INotifyCompletion
        {
            private readonly int _number;
            public bool IsCompleted => false;

            public AnyTypeAwaiter(int number)
            {
                _number = number;
            }
     
            public void OnCompleted(Action continuation)
            {
                continuation();
            }

            public int GetResult()
            {
                return _number * _number;
            }
        }
    }
Да, верно. Не совсем удачно написал, решил не загромождать постоянными вариациями для void и других возвращаемых. Поправлю в ближайшее свободное.
Если говорить без углубления в системные вызовы, то существует такая структура Overlapped. В ней есть важное нам поле — HANDLE hEvent.

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


Правильный механизм, через который происходит асинхронный ввод-вывод — это IOCP, порты завершения ввода-вывода.


Если интересует реализация, то первый нетривиальный код находится тут:


RandomAccess.QueueAsyncReadFile


Первой же строчкой он вызывает (через несколько посредников) ThreadPool.BindHandle, который уходит либо в нативный BindIOCompletionCallbackNative, либо в управляемый PortableThreadPool.RegisterForIOCompletionNotifications. В любом случае всё заканчивается вызовом CreateIoCompletionPort.

Вы путаете разные понятия.

В первом методе, который вы скинули, в возвращаемом значении упоминается OverlappedValueTaskSource.

Вы правы насчёт использования ИО портов. Это и не отрицается. Просто данная структура - это то, с помощью чего они используются.

https://docs.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/msmq/ms706972(v=vs.85)

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

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

Нет, это вы что-то путаете.


В первом методе, который вы скинули, в возвращаемом значении упоминается OverlappedValueTaskSource.

И что дальше? Из этого никак не следует, что поле hEvent является важным.


Структура Overlapped участвует в этом процессе и обьясняет, как вызывается продолжение асинхронной операции.

Структура Overlapped никак не объясняет как вызывается продолжение асинхронной операции. В ней нет поля для функции обратного вызова.

Неправда, есть.

Вот в C# типе для Overlapped
https://github.com/dotnet/runtime/blob/7bfc61b970e28f94782ef7c0cbcbbbc94ef9f5eb/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs#L70

А вот и конструктор OverlappedValueTaskSource, где видно, какой колбэк используется.
https://github.com/dotnet/runtime/blob/7bfc61b970e28f94782ef7c0cbcbbbc94ef9f5eb/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs#L66

В колбэке как раз вызывается этот метод. https://github.com/dotnet/runtime/blob/7bfc61b970e28f94782ef7c0cbcbbbc94ef9f5eb/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs#L200
В нем видно, как ставится результат операции.

Я считаю, это вполне достаточно в статье об асинхронности в C#.
Углубляясь дальше, можно потерять суть и уйти в дебри ОС и устройства дисков

Вы дали ссылку на OverlappedData, а не на системный Overlapped.
И вы всё ещё не показали где заполняется поле hEvent и почему оно так важно.


Я считаю, это вполне достаточно в статье об асинхронности в C#.
Углубляясь дальше, можно потерять суть и уйти в дебри ОС и устройства дисков

Причём тут углубление? У вас в статье написана ложная информация про использование поля hEvent.

Хорошо, был неправ. hEvent не используется

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории