Pull to refresh

Comments 25

Возможно, даже push-, pull- без перевода.

Ссылку исправил. Спасибо за наводку. А статья — перевод :) Потому и по ссылкам.

Мне очень нравятся простые примеры какой-то новой фичи, когда наглядно показывается, как раньше было всё плохо, а с новым нововведением стало всё хорошо. Пример с суммой чисел — это не то, чтобы я хотел видеть для асинхронных потоков.
Исправляем ситуацию… Допустим, у вас есть следующий метод
IEnumerable<FileResult> ReadFiles(string[] fileNames)
{
  foreach(var name in fileNames)
  {
    yield return ReadFile(name);
  }
}

Видите ключевое слово yield? В нем все дело.
Теперь вы узнали о чудесной конструкции async await и хотели бы применить ее к данному методу. И у вас даже есть новый метод ReadFileAsync.
Вы хотели бы, конечно, написать что-то такое
async IAsyncEnumerable<FileResult> ReadFilesAsync(string[] fileNames)
{
  foreach(var name in fileNames)
  {
    yield return await ReadFileAsync(name);
  }
}

Так вот! Раньше так нельзя было написать. А в C# 8 можно.
И в JavaScript тоже можно, кстати. Даже синтаксис такой же. Тот редкий случай, когда JS ненадолго обогнал C#.
Ну как обогнал… В js async/await пришел из ts, ts синтаксис разрабатывает одна мелкомягкая компания, которая и реализовала этот паттерн изначально в c#.
В js async/await пришел из ts

image
А причём тут вообще внезапно javascript? Тем более async/await в js и C# это несколько разные вещи.

А что там принципиально разного?

В C# таски могут выполняться в другой нити, а в js все асинхронные задачи выполняются в одной единственной нити.
Например поэтому в C# есть такой твик как ConfigureAwait()

Ну так это особенности рантайма вообще, а не особенности async/await.


И да, про задачу лучше не говорить что она "может выполняться в другой нити" — создаётся впечатление, что задача всегда выполняется в какой-то нити, а это не так.

Язык разработан с оглядкой на типичный рантайм (например в C# есть такие вещи как volatile и lock, имеющие смысл только в многопоточном рантайме). Так и же и async/await разработан с учётом того что таски могут выполняться в разных нитях: ConfigureAwait(). От этого может меняться тактика работы с async/await, например в C# async await может использоваться для распараллеливания cpu intensive задач, а в js в этом смысла нет.

Ещё один отличием является то обстоятельство что Promise`ы и Task`и не эквиваленты по фичам. Task`и подразумевают отмену операции, соответственно в месте await`а можно получить OperationCanceledException, в js ничего похоже в стандартизированного нет.

Вот только соответствующая фича Task'а в рамках async/await целиком сводится к всего лишь ещё одному исключению...

Асинхронные Stream в C# 8 как я понимаю не поддерживают обратного давления и для более или менее житейский ситуаций нужно использовать Rx?

Почему не поддерживают? Обратное давление как раз получится, если читать медленно (например, если внутри foreach await что-то медленное делается). Подтормаживаться будут точки yield-ов. И в этом преимущество по сравнению с Rx-подходом, где обратного давления нет совсем.
Обратное давление как раз получится, если читать медленно

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

Э-э-э, в каком смысле "следить"? Где в приведенном чуть выше коде вы видите ручное управление давлением?


  foreach(var name in fileNames)
  {
    yield return await ReadFileAsync(name);
  }
Нисколько не приуменьшаю этот новый функционал в C#. Для полноты картины от нужен 100%.

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

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

Один Task.Run в нужном месте (или ConfigureAwait(false)) — и вот уже основной поток не нагружается.

Можно и Task.Run, а можно и new Thread(). Вопрос, что дороже. Месить ThreadPool кусками кода, или в одном параллельном потоке сделать все, что можно и вернуть результат работы.

ConfigureAwait(false) отрывает Task от изначального синхронизационного контекста, бросая продолжения на ThreadPool, я так понимаю. Это значит, аллокировать делегат продолжения, зафиксировать все используемые локальные переменные из окружения, найти в ThreadPool свободный поток (а это thread safe, значит лочить надо), заинициализировать все статические переменные потока, чтобы Task-и работали нормально. Плюс там все через StateMachine сделано, т.е. каждый await это switch по int.

Надо замерять производительность. Меня сильно беспокоит то, что в UWP они начинают все API асинхронным делать, без альтернативных синхронных методов. Добавить сюда еще постоянный COM marshalling в сишное ядно, так недолго добиться JavaScript производительности. :)

Нет, ConfigureAwait(false) исполняет продолжение в том же самом потоке, в котором завершилась задача, если только задача не была создана с флагом RunContinuationsAsync

Разве?.. По логике вещей идет обычное планирование продолжения в ThreadPool и сначала будут сделаны запланированные ранее задачи, чем дело дойдет до этого продолжения. Иначе не будет разницы между обычными потоками без await. Нет никакой гарантии выполнения в том же потоке при ConfigureAwait(false), да и ConfigureAwait(true) тоже, если нет контекста, который жестко прибьет гвоздями продолжение к какому-то потоку.

Может тяготеть исполнять в том же потоке — да, но это не гарантируется.

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

При truе зависит от реализации SynchronizationContext.
Sign up to leave a comment.