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

Теперь это НЕ перевод. Это моя интерпретация тех частей содержания первой половины Поста: Как на самом деле Async/Await работают в C#, которые мне показались заслуживающими внимания в этой работе, с моими пояснениями относительно того почему у меня возникла именно такая интерпретация материала.

Судя по количеству просмотров, работа вызывает интерес, но пробиться сквозь нагромождение терминов трудно даже для подготовленного читателя, как мне кажется. Я хочу попробовать перевести не с английского на русский, а с некоторого кулуарно-профессионального на какой-то более доступный язык. Не знаю, насколько доступный, надеюсь получить некоторый отклик, который поможет мне понять, насколько у меня это получилось. Заранее хочу сказать, что автор действительно изложил как или во что компилируются конструкции Async/Await и, соответственно, как они работают изнутри. Проблема в том, что автору пришлось написать большую подготовительную часть чтобы подвести к изложению этого внутреннего устройства Async/Await. И мне, волей неволей, придется пройтись по всему что предваряет, собственно, основную идею в реализации Async/Await. Поэтому запаситесь терпением либо начинайте читать сразу последнюю часть.

Это обзор только первой половины Поста, возможно по результатам анализа второй половины или по вашим замечаниям мне придётся пересмотреть какие-то мои выводы, я не претендую на абсолютное знание.

Дисклеймер 1

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


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

Причем здесь потоки (threads), дисклеймер 2

Почему-то никто не пишет, что Асинхронный вызов — это всегда использование концепции потоков (threads) реализованной Операционной Системой и этот Пост, к сожалению, не исключение. Поэтому позвольте мне начать с базовых вещей, которые, как мне кажется, очень важны для понимания темы. Любой отдельный поток (thread) является объектом операционной системы и, соответственно, объектом под контролем ОС. Поток создается и удаляется только через обращения к операционной системе, через системные вызовы. Вызов, асинхронный по отношению к коду (функции, объекту,…) который выполняет этот вызов, возможен только при наличии дополнительного потока, в котором этот асинхронный вызов будет выполняться. Тогда вызывающий код фактически сможет продолжить свое исполнение в своем исходном потоке параллельно с исполнением кода асинхронной функции в дополнительном потоке, в этом и есть смысл асинхронности, в основном.

В основном все признают, что трудно найти задачу сложнее чем проектирование работы многопоточного приложения (читай приложения с асинхронными вызовами), потому что для асинхронных вызовов порядок завершения этих вызовов не определен. Добавляя в ваше приложение асинхронные вызовы, вы, по сути, добавляете в ваше приложение то, что именуется Undefined Behavior (неопределенное поведение). Например, если вы запустили всего три асинхронных процедуры, вы в общем случае должны проанализировать не менее 3! = 6 вариантов порядка завершения этих процедур, и это без учета того, что в некоторых случаях разного рода наложения разных событий при параллельном (асинхронном) исполнении функций (операций, процедур, кусков кода, …) нужно считать особым случаем формирования такого порядка исполнения. То есть, вообще говоря, даже для двух асинхронных вызовов нельзя заранее с уверенностью сказать сколько же вариантов взаимодействия событий, связанных с этими асинхронными процедурами, нужно рассмотреть и, соответственно, обработать в коде. К сожалению, никакой синтаксис вам не поможет даже просто найти все эти варианты, которые вы должны обработать в коде, потому что эти варианты определяются в предметной области задачи. Это всегда вопрос того, насколько хорошо вы сформулировали задачу предметной области и насколько хорошо проработали ее решение.

С точки зрения программирования, то есть перевода найденного решения в код, очевидно проще всего (надежнее), будет отказаться от асинхронных вызовов до тех пор, пока необходимость асинхронной работы будет не просто обоснована некоторым предполагаемым улучшением производительности кода, например. Асинхронные вызовы не рекомендуется применять пока необходимость асинхронной работы не доказана с математической точностью (в том смысле что она необходима и достаточна - то есть точно решает определенную проблему и не создает других не понятных проблем И/ИЛИ что проблемы, которые она создает, тоже гарантированно решаются каким-то образом).

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

Многопоточность (Multithreading) для практического программирования. То, о чем «забыть-нельзя-вспоминать» придется

Многопоточность (Multithreading) для практического программирования

Плавно переходим к анализу содержания Поста

Так вот, пусть мы увидели-поняли откуда берется сложность систем с асинхронными процедурами и, казалось бы, не может быть ничего сложнее. Но не тут-то было! Сложнее чем система с асинхронными процедурами, может быть система, в которой асинхронные процедуры иногда могут выполняться синхронно! То есть нам в систему добавляется еще один уровень неопределенности, это как раз то, о чем нам как бы между делом сообщают в Посте как о каком-то малозначимом факте, который не плохо бы тоже иметь в виду (видимо иногда):

And “asynchronous” operations that complete synchronously are actually very common; they’re not “asynchronous” because they’re guaranteed to complete asynchronously but rather are just permitted to.

Типа: «Знаете ли, асинхронные операции могут, вообще-то, выполняться как синхронные и это как бы тоже проблема!», но как раз этой проблеме посвящена первая глава Поста, которая вроде бы посвящена APM паттерну, исходя из названия. По сути, первая глава просто констатирует наличие проблемы, связанной с тем, что асинхронные вызовы могут выполняться синхронно, в основном!

«stack dives» это же рекурсия?

Идентифицированная нами проблема выливается в проблему со стеком (стек — это понятие, жестко связанное с потоком-thread, не забываем про связь с потоками). Автор применил специальный термин для нее: «stack dives». Что такое stack dives? это же широко известное давно используемое понятие, это рекурсия! Проверьте меня, это всего лишь на всего рекурсия! Зачем вместо широко известного термина «рекурсия» изобретать какое-то вычурное «погружение в стек»? У меня есть одно предположение.

Есть много мастеров программирования которые пропагандируют методы построения алгоритмов, основанные на рекурсии. Все эти методы игнорируют простой медицинский факт, что неконтролируемая рекурсия ведет к переполнению стека, которое провоцирует аварийное завершение приложения. Я не видел ни одного алгоритма, основанного на рекурсии в котором бы была прописана или анализировалась допустимая глубина используемой рекурсии для безопасного выполнения этого алгоритма на ПК (на вычислительной машине). Самое противное при аварийном завершении приложения по причине переполнения стека это то, что во многих системах переполнение стека ведет еще и к разрушению базы отладочной информации и вам придется приложить немало усилий, чтобы понять почему же ваше приложение (служба, библиотека, …) упала (обычно в самый не подходящий момент), а виной всему будет рекурсия. До сих пор никто не вводил стандартных методов предупреждения возможного переполнения стека при очередном вызове функции, и на сколько я знаю, никто не собирается такой метод изобрести. Поэтому неконтролируемая рекурсия остается миной замедленного действия в любой программе.

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

SynchronizationContext контекст синхронизации

Асинхронный вызов возможен либо через создание нового потока для выполнения вызываемой функции асинхронно, либо через отправку сообщения некоторому существующему потоку. Назовем такой поток вторым потоком. Второй поток должен выполнить эту функцию, взятую из сообщения, с параметрами, которые могут прийти в том же сообщении либо как-то еще. С любым и каждым потоком связан контекст этого потока, который включает в себя как минимум стек потока и атрибуты потока как системного объекта, Пост знакомит нас с другим понятием: SynchronizationContext контекст синхронизации, которое принадлежит некоторому планировщику (scheduler), в качестве которого может выступать опять же системный поток, а может некоторая абстрактная операция, которая иногда асинхронная, а иногда нет. В принципе этот новый класс контекста нужен (моя догадка) в том числе чтобы разрешить эту добавленную неопределенность относительно того является ли данная операция асинхронной (выполняемой в стороннем потоке – в контексте стороннего потока ИЛИ она выполняется в текущем потоке, а значит выполняется как синхронная).

Чтобы обосновать свою догадку я процитирую какой планировщик может представлять SynchronizationContext в примере от автора Поста:

The base implementation of SynchronizationContext, for example, just represents the ThreadPool, and so the base implementation of SynchronizationContext.Post simply delegates to ThreadPool.QueueUserWorkItem, which is used to ask the ThreadPool to invoke the supplied callback with the associated state on one the pool’s threads.

Здесь написано, что SynchronizationContext может представлять ThreadPool, который в свою очередь выделяет поток для выполнения функции callback при передаче этой функции в SynchronizationContext через метод SynchronizationContext.Post. Но насколько я понимаю, в конечном итоге SynchronizationContext будет представлять конкретный поток, который будет выделен для исполнения операции.

Насколько я могу судить, основной смысл главы

Event-Based Asynchronous Pattern

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

Если не верите переведите вот эту цитату:

However, it did add one notable advance that the APM pattern didn’t factor in at all, and that has endured into the models we embrace today: SynchronizationContext.

continuation Функция которая должна выполняться при завершении асинхронной операции.

Мы пропустили термин, который автор Поста вводит в рассмотрение в самом начале: continuation или продолжение, но который он никак не выделяет далее в тексте Поста. Действительно, в начале Поста совершенно не понятно почему же автор так озаботился о том, а как же нам задать-передать функцию, которая будет вызываться после завершения асинхронной операции, и как бы не проморгать «стек оверфлоу» при вызове этой функции, полученный по причине возможной рекурсии из-за того, что асинхронную операцию вызвали как синхронную. Не самая короткая логическая цепочка получилась, согласитесь. Я бы хотел научиться так же красиво рассказывать про такие, очень непростые логические цепочки как это сделал автор Поста!

Дело в том, что эта функция, это «продолжение» играет ключевую роль в том, во что компилируется Async/Await (играет ключевую роль в построении реализации конструкций на основе Async/Await). И это действительно достаточно интересная техника. Но для начала давайте посмотрим, что автор Поста выделяет относительно типа Task из .NET Framework 4.0, или что мне показалось заслуживающим внимания по поводу этого типа Task у автора Поста.

Введение по Таскам(Tasks) от автора поста

Так вот, тут автор Поста, естественно, пишет, я бы даже сказал восторженно пишет, что Таски(Tasks) гораздо лучше, чем IAsyncResult. Я вполне могу согласится с его восторгом, только он совсем не способствует пониманию темы, в данном случае, а скорее отвлекает, поэтому мы опустим эти детали.

Как ни странно, содержательная часть этого введения по Таскам(Tasks) опять же связана с continuation или продолжением! Автор Поста пишет, что Таски позволяют определять «продолжение» не только ДО вызова (для Тасков больше подходит: создание) асинхронных операций, но и во время их исполнения и даже после завершения, цитата:

As noted, one of the fundamental advances in Task over previous models was the ability to supply the continuation work (the callback) after the operation was initiated.

Тут стоит отметить, я думаю, последовательную линию автора Поста по поиску того что может заменить то, что он называет callback-based asynchrony или callback-based approaches, цитата из первой главы:

this isn’t really a criticism of the APM pattern. Rather, it’s a critique of callback-based asynchrony in general. We’re all so used to the power and simplicity that control flow constructs in modern languages provide us with, and callback-based approaches typically run afoul of such constructs once any reasonable amount of complexity is introduced.

Цитата из раздела "And ValueTasks":

So, we have TaskTask<TResult>ValueTask, and ValueTask<TResult>. We’re able to interact with them in various ways, representing arbitrary asynchronous operations and hooking up continuations to handle the completion of those asynchronous operations. And yes, we can do so before or after the operation completes.

But… those continuations are still callbacks!

...

How can we fix that????

Тут хочется заметить еще, по поводу определения Тасков сформулированного автором. Я не могу согласиться с тем, что Таски представляют только «возможное завершение какой-либо асинхронной операции» (the eventual completion of some asynchronous operation). Мне кажется они определены там таким образом, только для того чтобы написать далее про аналогию с типами из других Фреймворков: a “promise” or a “future”.

Мне все-таки кажется (то есть я уверен, потому что это очевидно, по-моему) что Таски(Tasks) представляют всю целую задачу или операцию вместе с ее результатом, когда она завершилась, и с ее состоянием в любой момент времени.

Во что компилируется Async/Await

 Начнем, пожалуй, сразу с ответа. Автор поста в конце главы

C# Iterators to the Rescue

пишет (почти дословный перевод): что-то около 95% логики для поддержки итераторов (как возвращаемое значение функции которая возвращает IEnumerable<Т> ) используется также и для реализации конструкций на базе Async/Await.

Соответственно, вся эта глава про итераторы посвящена анализу логики компилятора для преобразования функций использующих yield return для генерации IEnumerable<> списка. Такую хитрую конструкцию невозможно анализировать без примера, и конечно такой пример нам предоставлен:

public static IEnumerable<int> Fib()
{
    int prev = 0, next = 1;
    yield return prev;
    yield return next;

    while (true)
    {
        int sum = prev + next;
        yield return sum;
        prev = next;
        next = sum;
    }
}

В качестве результата компиляции примера мы можем видеть, что функция из примера разворачивается компилятором C# в полноценную стейт-машину (конечный автомат, или проще говоря, в функцию с одним большим оператором switch, которую вы найдете в исходной статье или в переводе на Хабре).

Тут мы подошли к основной идее, которую раскрывает для нас автор поста. Конструкция Async/Await (функция помеченная async) тоже разворачивается компилятором C# в полноценную стейт-машину, в которой блоки кода под состояниями определяются (отделяются в исходном коде) модификатором await, так же как они определяются с помощью yield return для итерируемых функций. Чтобы эта полученная стейт машина могла работать, компилятор генерирует (или имеет встроенную) функцию, приблизительную версию кода которой нам демонстрирует автор Поста:

static Task IterateAsync(IEnumerable<Task> tasks)
{
    var tcs = new TaskCompletionSource();

    IEnumerator<Task> e = tasks.GetEnumerator();

    void Process()
    {
        try
        {
            if (e.MoveNext())
            {
                e.Current.ContinueWith(t => Process());
                return;
            }
        }
        catch (Exception e)
        {
            tcs.SetException(e);
            return;
        }
        tcs.SetResult();
    };
    Process();

    return tcs.Task;
}

Обратите внимание на метод ContinueWith() в строчке:

e.Current.ContinueWith(t => Process());

Как можно видеть продолжение играет немаловажную роль после преобразования исходного C# кода в промежуточный исполняемый код. Вот почему ранее автор столько внимания уделил continuation – функции, которая должна выполняться при завершении асинхронной операции, как мне кажется. Эта callback-функция незаменима при компиляции конструкций Async/Await.

Здесь стоит перевести цитату с пояснениями:

Different syntax, different types involved, but fundamentally the same transform. Squint at the yield returns, and you can almost see awaits in their stead.

=

Другой синтаксис (Impl() вместо MoveNext(), await вместо yield return), задействованы другие типы (Task вместо IEnumerable<>), но по сути одно и то же преобразование (преобразование в стейт машину компилятором!). Присмотритесь к yield return, и вы почти можете увидеть await вместо них (вот мы, вроде как, и присмотрелись!).

На этом пока все.

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

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

Поэтому пишите, что вы считаете важным, полезным или наоборот недосказанным, непонятным в этой работе, в моей и в исходном .Net Посте.

С уважением,

Сёргий

Комментарии (22)


  1. Deosis
    26.12.2023 03:58
    +8

    Почему-то никто не пишет, что Асинхронный вызов — это всегда использование концепции потоков (threads) реализованной Операционной Системой

    Потому что это неправда. Простой пример: горутины не равны потокам. Можно асинхронный код заставить выполнятся в одном потоке, от этого он не станет менее асинхронным.


    1. olivera507224
      26.12.2023 03:58

      Полностью поддерживаю. Очень странно видеть в тексте, который описывает понимание автором базовых вещей, принципиальную ошибку уровня новичка, в которой утверждается что Асинхронность всегда подразумевает Многопоточность. В том же С# концепция async/await подразумевает работу с тасками, которые все одновременно вполне могут работать как в одном потоке, так и в нескольких - как решит рантайм. В случае большого количества мелких тасков, генерирующих iobound, нет смысла запускать их в отдельных потоках, потому что создание потоков - удовольствие не из дешёвых. По уровню абстракции таски в C# примерно сопоставимы с горутинами в Go.

      Даже немного страшно читать дальше.


      1. rukhi7 Автор
        26.12.2023 03:58
        -3

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

        Если это и ошибка, то это ошибка уровня СТАричка, а не новичка, потому что Многопоточность появилась и успешно применялась задолго до появления концепций по которым мы дискутируем (я надеюсь это то слово которое следует здесь применять).

        Кстати, если вдруг(внезапно) выяснится что ошибка есть с другой стороны, то это действительно смело можно будет именовать Ошибкой Новичка.

        Даже немного страшно читать дальше.

        А я боюсь за этим может последовать: надо сжечь, желательно вместе с автором. Это мне что-то напоминает из совсем древних времен, ничего не меняется?


        1. mayorovp
          26.12.2023 03:58
          +1

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

          Если же вы свои новичковые ошибки оправдываете тем, что вы якобы "старичок" - поздравляю, вы не "старичок", вы динозавр.


          1. rukhi7 Автор
            26.12.2023 03:58
            -2

            Статья на которую вы ссылаетесь, начинается оксюмороном:

             должен быть поток, в котором выполняется ожидание!

            я поясню: словосочетание "выполнять ожидание" является внутренне противоречивым, вы не находите?

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

            Но как я понимаю вас это нисколько не смущает. Ну хорошо, у меня появилась тема для новой статьи. Спасибо.


            1. mayorovp
              26.12.2023 03:58

              Почему вы эти вопросы задаёте тут, а не в комментариях к той статье?

              я поясню: словосочетание "выполнять ожидание" является внутренне противоречивым, вы не находите?

              Нет, не нахожу.

              Далее автор той статьи доказывает что для выполнения ожидания потока нет, вопросы: а что он ожидал?

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

              А если рассмотреть операцию для которой все таки нужно производить какие то реальные вычисления

              А давайте всё-таки рассматривать операцию, для которой асинхронность придумывалась? Ежу понятно, что для вычислений нужен поток, проблема в том что некоторые пытаются что угодно свести к вычислениям.


              1. rukhi7 Автор
                26.12.2023 03:58
                -2

                Ежу понятно, что для вычислений нужен поток, проблема в том что некоторые пытаются что угодно свести к вычислениям.

                Вот это интересно, вроде как компьютер ничего кроме как вычислений делать не умеет, оно же и переводится как "вычислитель". А выполнять ожидание вроде как значит "ничего не делать", то есть делать ничего-не-делание какое-то получается, в этом противоречивость, по моему. То есть мы насчитали 2 варианта:

                1.вычислять

                2.ничего не делать

                или вы можете другие варианты предложить? только не надо предлагать варианты типа

                ничего не делать (то есть ждать) того

                ничего не делать (то есть ждать) этого

                ...


                1. mayorovp
                  26.12.2023 03:58

                  Рассмотрим следующий код (да, так писать нельзя, но как контрпример сойдёт):

                  bool flag;
                  
                  // ...
                  
                  while(!flag);

                  Что этот код делает? Ждёт.

                  Вычисляет ли этот код хоть что-то? Нет.

                  Занимает ли он поток? Да.

                  Занимает ли он процессор? Да.


                  1. rukhi7 Автор
                    26.12.2023 03:58
                    -2

                    вы хотите сказать что можно ничего не делать, при что-то делая?

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

                    Только я не понимаю к чему это вы, что это доказывает?

                    Я надеюсь это не вы минусы ставите, иначе глупо бы было вам что-то спрашивать. Вот в такой токсичной атмосфере приходится работать :( !


                    1. mayorovp
                      26.12.2023 03:58
                      +1

                      Я к тому, что все те, безусловно, очень тяжелые вычисления, перечисленные вами, обычные люди, не являющиеся троллями, называют "ожиданием". Более точно - активным ожиджанием.


        1. olivera507224
          26.12.2023 03:58

          Многопоточность появилась и успешно применялась задолго до появления концепций по которым мы дискутируем (я надеюсь это то слово которое следует здесь применять).

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

          А я боюсь за этим может последовать: надо сжечь, желательно вместе с автором.

          Нет, конечно, я не сторонник чмырения за незнание. Если ты не знаешь и хочешь узнать, то мне не трудно накидать тебе статей по сабжу.


          1. rukhi7 Автор
            26.12.2023 03:58
            -1

            вот это, извините, хамство. Успеете пост удалить?


            1. olivera507224
              26.12.2023 03:58

              В чем заключается хамство?


  1. mayorovp
    26.12.2023 03:58
    +4

    Лучше бы вы и дальше просто переводили...

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

    Нет никакого потока

    Автор применил специальный термин для нее: «stack dives». Что такое stack dives? это же широко известное давно используемое понятие, это рекурсия! Проверьте меня, это всего лишь на всего рекурсия! Зачем вместо широко известного термина «рекурсия» изобретать какое-то вычурное «погружение в стек»? У меня есть одно предположение.

    Потому что кто ему запретит? Вы вот точно всегда используете термины, или иногда заменяете их метафорами? Уверены, что я не найду ни одной метафоры если поищу по всем вашим публикациям?

    А вы уже успели на основе одной метафоры сделать далеко идущие выводы...

    В принципе этот новый класс контекста нужен (моя догадка) в том числе чтобы разрешить эту добавленную неопределенность относительно того является ли данная операция асинхронной (выполняемой в стороннем потоке – в контексте стороннего потока ИЛИ она выполняется в текущем потоке, а значит выполняется как синхронная). [...] Но насколько я понимаю, в конечном итоге SynchronizationContext будет представлять конкретный поток, который будет выделен для исполнения операции.

    Вроде у автора все понятно написано, почему у вас какие-то странные догадки? Сначала написано что он делает, а потом - для чего это нужно. Но, судя по вашим цитатам, вы просто бросили читать на этом моменте.

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

    Действительно, в начале Поста совершенно не понятно почему же автор так озаботился о том, а как же нам задать-передать функцию, которая будет вызываться после завершения асинхронной операции

    Вот это вы серьёзно спрашиваете, почему же автор озаботился тем, чтобы получить и обработать результат операции?!

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

    Дело в том, что эта функция, это «продолжение» играет ключевую роль в том, во что компилируется Async/Await (играет ключевую роль в построении реализации конструкций на основе Async/Await). 

    Нет, не ставьте телегу впереди лошади. Продолжения не появились по воле авторов компилятора. Напротив, код компилируется именно так, как он компилируется, потому что нам нужны продолжения.

    Мне все-таки кажется (то есть я уверен, потому что это очевидно, по-моему) что Таски(Tasks) представляют всю целую задачу или операцию вместе с ее результатом, когда она завершилась, и с ее состоянием в любой момент времени.

    При создании через устаревший конструктор - да, представляют.

    Но в современном использовании они представляют исключительно результат задачи. Аналогия с promise и future не случайна и полностью справедлива.

    Чтобы эта полученная стейт машина могла работать, компилятор генерирует (или имеет встроенную) функцию, приблизительную версию кода которой нам демонстрирует автор Поста:

    Ну уж нет, при компилятор генерирует напрямую конечный автомат для асинхронного кода. То, что привёл автор поста - лишь способ превратить yield return в await, компилятору такие костыли не требуются.


    1. rukhi7 Автор
      26.12.2023 03:58
      -4

      Нет никакого потока

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

      await myDevice.WriteAsync(data, 0, data.Length);

      где автор НЕ обнаружил потока, асинхронная операция превратилась в синхронную.

      Но...

      Но Вы так уверены в ваших знаниях, что я не возьмусь даже пытаться вас переубедить.


      1. mayorovp
        26.12.2023 03:58
        +1

         асинхронная операция превратилась в синхронную

        С чего бы?


      1. ColdPhoenix
        26.12.2023 03:58
        +1

        асинхронная операция превратилась в синхронную.

        Тогда бы интерфейс завис(например в WPF)

        Естественно если совсем уж уходить в глубину, то потоки есть, ОС же сама никуда не делась(с ее потоками). НО, там нет потоков созданных приложением. Системные потоки в таком случае не учитываются(например потоки для поддержания IO Completion Port).

        Но, я трансформировал код аниматора на Android вместо колбэков на async/await, там даже системные потоки не использовались в итоге, все идет в одном UI потоке.(сам аниматор естественно тоже в UI потоке работает)

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

        Кстати говоря, уже есть же цикл статей про async/await.

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


        1. rukhi7 Автор
          26.12.2023 03:58

          Кстати говоря, уже есть же цикл статей про async/await.

          Да! у меня упоминается этот перевод уже есть на Хабре  под самым заголовком. На мой взгляд этот перевод - почти машинный перевод.

          Тогда бы интерфейс завис(например в WPF)

          Так там да, просто вызывающая функция распадается на кейсы под свичем.

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

          Про SynchronizationContext я до конца не написал еще, возможно еще напишу.


          1. ColdPhoenix
            26.12.2023 03:58

            Так там да, просто вызывающая функция распадается на кейсы под свичем.

            Я знаю как это работает, я про превращение операции в синхронную.

            На мой взгляд этот перевод - почти машинный перевод.

            Она просто перевод, без догадок. Чем гораздо полезнее.


  1. ryanl
    26.12.2023 03:58
    +1

    Но насколько я понимаю, в конечном итоге SynchronizationContext будет представлять конкретный поток, который будет выделен для исполнения операции.

    Нет, нет и нет. SynchronizationContext - это абстракция, которая определяет специфику выполнения кода по завершению асинхронной операции (где, когда, с какими деталями). В .NET-е всегда было хорошее интуитивно-понятное именование типов. Почему в слове SynchronizationContext вы увидели поток? Правильно говорил С.Тепляков в одной из своих русскоязычных статей, что всех профессионалов объединяет одно - осторожность суждений. А вы даже в письменной форме в выражении своих мыслей умудрились накосячить. Не пахнет тут профессионализмом.

    Вот почему я за то, чтобы читать статьи в оригинале, а не читать потом догадки переводчиков, со всеми их неправильными интуициями в сложных вопросах. НЕ НАДО продолжать публикации ваших догадок по статьям, описывающих сложные темы. Грамотные, опытные разработчики сами разберутся, что да как.
    Как уже выше сказали, продолжайте просто переводить.
    PS: И это только по одному абзацу замечание...


    1. rukhi7 Автор
      26.12.2023 03:58
      -2

       И это только по одному абзацу замечание..

      Что-то вы один абзац по которому есть замечание нашли уже ближе к концу. Но я этот абзац специально написал чтобы было к чему придраться! Меня убивает отсутствие комментариев - когда тихо как в могиле.

      Поэтому, благодарю за замечание!


      1. ryanl
        26.12.2023 03:58
        +3

        Что-то вы один абзац по которому есть замечание нашли уже ближе к концу.

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

        Меня убивает отсутствие комментариев - когда тихо как в могиле.

        Тема, на самом деле, уже давно изъезжена вдоль и поперек.