Страницы

Поиск по вопросам

пятница, 10 января 2020 г.

Туманности и мои пробелы в знаниях с async\await и Task'ами в целом

#c_sharp #async_await #async_programming #task #context


Здравствуйте, я захотел опробовать такую вкусняшку C# как async/await и написал тестовую
программу:

class MySynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback d, object state)
    {
        Console.WriteLine("Post");
        base.Post(d, state);
    }
    public override void Send(SendOrPostCallback d, object state)
    {
        Console.WriteLine("Send");
        base.Send(d, state);
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());
        var browsers = GetBrowsers();
        Console.WriteLine("start");
        Console.WriteLine(browsers.Result);
    }
    static async Task GetBrowsers()
    {
        var res = string.Empty;
        var bd = await GetFromFS();
        res += bd;               // <-тут могла быть операция с UI
        var net = await GetFromNet();
        res += " and " + net;   // <-тут могла быть операция с UI
        var cpu = await Task.Run(() => { Thread.Sleep(200); return "edge"; });
        res += " and " + cpu;   // <-тут могла быть операция с UI
        return res;
    }
    static async Task GetFromFS()
    {
        using (var sr = new StreamReader(@"C:\bd.txt"))
        {
            var res = await sr.ReadToEndAsync();
            // тут могла быть операция с UI
            return res + " and firefox";
        }
    }
    static async Task GetFromNet()
    {
        await Task.Delay(200);
        // тут могла быть операция с UI
        return "chrome";
    }

}


Я ожидал, что после каждого await'а мой контекст будет восстанавливаться (ведь будь
это приложение с UI, то надо же получать доступ к контролам), а значит, что после каждого
await'а в консоль будет выводится "Post", но каково было мое удивление когда вывелся
только 2 раза. Отсюда вопрос:


почему контекст привязывается не всегда?


Теперь к async/await: как я понимаю, начиная с var browsers = GetBrowsers(); вызов
будет проводится по стеку вниз до первого таска (в данном случае sr.ReadToEndAsync();,
а может и ниже), и управление сразу перейдет обратно в метод Main(), не дожидаясь завершения
этого таска; остальная часть async-методов будет выполняться после выполнения данного
таска и так далее, из чего возникает следующий вопрос:


кто ждет тот самый первый таск (затем следующие, когда до них дойдет
очередь после await'а), изменяет его состояние на
RanToCompletion? Думаю, было бы глупо думать, что для ожидания
выделяется поток из пула потоков, да?


Третий вопрос стоит на стыке тасков и пула потоков. Продолжение после await, как
я понимаю, выполняется в пуле потоков (там ему передается контекст), но...


что засовывает часть кода после await в пул потоков и когда (когда
таск завершился или когда создался?)?


Заранее спасибо за прояснение ситуации
    


Ответы

Ответ 1



Дело в том, что выполнение метода Post() в вашем контексте вы делегируете базовому методу. А базовый метод ставит делегат на выполнение в пуле потоков. В случае с UI такого не происходит, потому что, например, WinForms контекст полностью переопределяет методы. Вставим логирование контекста и текущего потока в ваш код: class MySynchronizationContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { Console.WriteLine($"Post, thread id = {Thread.CurrentThread.ManagedThreadId}"); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine($"Send, thread id: {Thread.CurrentThread.ManagedThreadId}"); base.Send(d, state); } public override string ToString() { return "My"; } } public class Program { public static void Main(string[] args) { SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext()); var browsers = GetBrowsers(); Console.WriteLine("start"); Console.WriteLine(browsers.Result); } static async Task GetBrowsers() { LogCurrentContext("GetBrowsers prologue"); var res = string.Empty; var bd = await GetFromFS(); LogCurrentContext("GetBrowsers after GetFromFS"); res += bd; // <-тут могла быть операция с UI var net = await GetFromNet(); LogCurrentContext("GetBrowsers after GetFromNet"); res += " and " + net; // <-тут могла быть операция с UI var cpu = await Task.Run(() => { Thread.Sleep(200); return "edge"; }); LogCurrentContext("GetBrowsers after Task.Run"); res += " and " + cpu; // <-тут могла быть операция с UI return res; } private static void LogCurrentContext(string message) { Console.WriteLine($"{message}: {(SynchronizationContext.Current?.ToString() ?? "default")} context, thread id = {Thread.CurrentThread.ManagedThreadId}"); } static async Task GetFromFS() { LogCurrentContext("GetFromFS prologue"); using (var sr = new StreamReader(@"D:\GetEventsMarkets.sql")) { var res = await sr.ReadToEndAsync(); LogCurrentContext("GetFromFS after ReadToEndAsync"); // тут могла быть операция с UI return res + " and firefox"; } } static async Task GetFromNet() { LogCurrentContext("GetFromNet prologue"); await Task.Delay(200); // тут могла быть операция с UI LogCurrentContext("GetFromNet after Task.Delay"); return "chrome"; } } Чаще всего я получал подобный результат: GetBrowsers prologue: My context, thread id = 1 GetFromFS prologue: My context, thread id = 1 Post, thread id = 1 GetFromFS after ReadToEndAsync: default context, thread id = 3 start Post, thread id = 3 GetBrowsers after GetFromFS: default context, thread id = 3 GetFromNet prologue: default context, thread id = 3 GetFromNet after Task.Delay: default context, thread id = 4 GetBrowsers after GetFromNet: default context, thread id = 4 GetBrowsers after Task.Run: default context, thread id = 4 Здесь нам интересны две вещи. В кастомном контексте вызываются только прологи методов GetBrowsers() и GetFromFS(). После того, как выполняется строка await sr.ReadToEndAsync(), продолжение метода GetFromFS() постится в кастомный контекст. Это первый вызов Post(). Далее метод GetFromFS() завершается и продолжение метода GetBrowsers() снова постится в кастомный контекст. Это второй вызов Post(). Однако поскольку кастомный контекст просто запускает код в пуле потоков, эти продолжения работают уже в контексте пула потоков. Именно поэтому мы больше не видим вызовов кастомного контекста. Метод GetFromFS() начал исполняться в потоке с id=1. Однако само продолжение при этом выполнялось в потоке с id=3 по причине, описанной выше. Продолжение после вызова GetFromFS() было вызвано в этом же потоке (Post, thread id = 3). "Самый первый таск", как и любой другой, ждет специальный IO поток (т.н. IO completion port, IOCP). Но такие потоки ждут очень большое количество завершений, в т.ч. и тасков, поэтому говорить о "один таск -- один ждущий поток" не приходится. Нырнуть вглубь и почитать подробнее можно в статье на Хабре. Продолжение async метода выполняется в захваченном контексте. Если такого контекста нет (например, при вызове с ConfigureAwait(false)) -- продолжение выполняется в контексте пула потоков. Вызов продолжение в соответствующем контексте выполняется компилятором -- он генерирует соответствующий код с вызовом Post(). Компилятор разбирает async метод на составляющие (пролог+продолжения) и генерирует из них стейт-машину с переходами. Очень рекомендую посмотреть это выступление (или хотя бы слайды), а также ознакомиться с этим ответом. После этого фразы вроде "async/await занимается переключением контекста" должны пропасть из вашего обихода. Как резюме: вы получили смущающие результаты потому, что ваша реализация контекста, строго говоря, некорректна. По сути она аналогична контексту пула потоков, который используется для консольных приложений и в котором исполняются все продолжения, если не был обнаружен другой контекст.

Ответ 2



почему контекст привязывается не всегда? Проблема вашего контекста - в том, что он не умеет восстанавливать себя. Если вы пишите свой контекст - то вы сами должны позаботиться чтобы все продолжения запускались в нем же. кто ждет тот самый первый таск Если асинхронность правильная - то "самый первый" Task, как и все последующие, создается при помощи механизма TaskCompletionSource. Например, так (код привожу только для примера, в реальности надо еще исключения обрабатывать): Task VeryFirstTask() { var tcs = new TaskCompletionSource(); Action handler = null; handler = () => { tcs.SetResult(42); SomeEvent -= handler; }; SomeEvent += handler; return tcs.Task; } Не обязательно используются события - но идея одна и та же. Где-то сохраняется TaskCompletionSource, у которого в нужный момент вызывается SetResult/SetException/SetCanceled. что засовывает часть кода после await в пул потоков и когда (когда таск завершился или когда создался?)? Напрямую его туда засовывает контекст синхронизации. Опосредовано в этом участвует так же такая структура данных как TaskAwaiter (она хранит захваченный контекст синхронизации).

Комментариев нет:

Отправить комментарий