Русский
Русский
English
Статистика
Реклама

Async/await

Перевод Использование глобального await в JavaScript

19.10.2020 14:23:26 | Автор: admin


Новая возможность, которая может изменить наш подход к написанию кода

JavaScript очень гибкий и мощный язык, который определяет развитие современного веба. Одна из основных причин главенства JavaScript в сфере веб-разработки заключается в его стермительном развитии и постоянном совершенствовании.

Одним из предложений по улучшению JavaScript является предложение под названием top-level await (await верхнего уровня, глобальный await). Цель данного предложения состоит в превращении ES модулей в некое подобие асинхронных функций. Это позволит модулям получать готовые к использованию ресурсы и блокировать модули, импортирующие их. Модули, которые импортируют ожидаемые ресурсы, смогут запускать выполнение кода только после получения ресурсов и их предварительной подготовки к использованию.

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

Не переживайте из-за этого. Продолжайте читать. Я покажу, как можно использовать названную фичу уже сейчас.

Что не так с обычным await?


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

await Promise.resolve(console.log("")); // Ошибка(async () => {    await Promise.resolve(console.log(""))})();

Указанная проблема и ее решение это лишь вершина айсберга


При работе с ES6 модулями вы, как правило, имеете дело с большим количеством экземпляров, экспортирующих и импортирующих значения. Рассмотрим пример:

// library.jsexport const sqrt = Math.sqrt;export const square = (x) => x * x;export const diagonal = (x, y) => sqrt((square(x) + square(y)));// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// IIFE(async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

В приведенном примере мы экспортируем и импортируем переменные между library.js и middleware.js. Вы можете назвать файлы как угодно.

Функция delay возвращает промис, разрешающийся после задержки. Поскольку данная функция является асинхронной, мы используем ключевое слово await внутри IIFE для ожидания ее завершения. В реальном приложении вместо функции delay будет вызов fetch (запроса на получение данных) или другая асинхронная задача. После разрешения промиса, мы присваиваем значение нашей переменной. Это означает, что до разрешения промиса наша переменная будет иметь значение undefined.

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

Давайте взглянем на код, в котором эти переменные импортируются и используются:

// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // undefinedconsole.log(diagonalOutput); // undefinedconsole.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Если вы запустите этот код, то в первых двух случаях получите undefined, а в третьем и четвертом 169 и 13, соответственно. Почему так происходит?

Это происходит из-за того, что мы пытаемся получить значения переменных, экспортируемых из middleware.js, в main.js до завершения выполнения асинхронной функции. Вы помните, что у нас там промис, ожидающий разрешения?

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

Обходные пути

Существует, как минимум, два способа решить обозначенную проблему.

1. Экспорт промиса для инициализации

Во-первых, можно экспортировать IIFE. Ключевое слово async делает метод асинхронным, такой метод всегда возвращает промис. Вот почему в приведенном ниже примере асинхронное IIFE возвращает промис.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневр или, как еще говорят, костыльexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

При получении доступа к экспортируемым переменным в main.js можно подождать выполнения IIFE.

// main.jsimport promise, { squareOutput, diagonalOutput } from "./middleware.js";promise.then(() => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

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

  • При использовании указанного шаблона приходится искать нужный промис
  • Если в другом модуле также используются переменные squareOutput и diagonalOutput, мы должны обеспечить реэкспорт IIFE

Существует и другой способ.

2. Разрешение промиса IIFE с экспортируемыми переменными

В данном случае, вместо экспорта переменных по-отдельности, мы возвращаем их из нашего асинхронного IIFE. Это позволяет файлу main.js просто ждать разрешения промиса и извлекать его значение.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневрexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);    return { squareOutput, diagonalOutput };})();// main.jsimport promise from "./middleware.js";promise.then(({ squareOutput, diagonalOutput }) => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Однако у такого решения также имеются некоторые недостатки.

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

Как глобальный await решает данную проблему?


await верхнего уровня позволяет модульной системе заботиться о разрешении промисов и их взаимодействии между собой.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// "глобальный" awaitawait delay(1000);squareOutput = square(13);diagonalOutput = diagonal(12, 5);export { squareOutput, diagonalOutput };// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // 169console.log(diagonalOutput); // 13console.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Ни одна из инструкций в main.js не выполняется до разрешения промисов в middleware.js. Это гораздо более чистое решение по сравнению с обходными путями.

Заметка

Глобальный await работает только с ES модулями. Используемые зависимости должны быть указаны явно. Приведенный ниже пример из репозитория предложения хорошо это демонстирует.

// x.mjsconsole.log("X1");await new Promise(r => setTimeout(r, 1000));console.log("X2");// y.mjsconsole.log("Y");// z.mjsimport "./x.mjs";import "./y.mjs";// X1// Y// X2

Данный сниппет не выведет в консоль X1, X2, Y, как можно ожидать, поскольку x и y отдельные модули, не связанные между собой.

Я настоятельно рекомендую вам изучить рездел часто задаваемые вопросы предложения для лучшего пониманию рассматриваемой фичи.

Реализация


V8

Вы можете протестировать данную возможность уже сейчас.

Для этого перейдите в директорию, в которой размещается Chrome на вашей машине. Убедитесь в том, что все вкладки браузера закрыты. Откройте терминал и введите следующую команду:

chrome.exe --js-flags="--harmony-top-level-await"

Вы таже можете попробовать данную фичу в Node.js. Прочитайте это руководство, чтобы узнать больше.

ES модули

Убедитесь, что добавили тегу script атрибут type со значением module.

<script type="module" src="./index.js"></script>

Обратите внимание, что в отличие от обычных скриптов, ES6 модули следуют политике общего происхождения (одного источника) (SOP) и совместного использования ресурсов (CORS). Поэтому с ними лучше работать на сервере.

Случаи использования


Согласно предложению случаями использования глобального await является следующее:

Динамический путь зависимости

const strings = await import(`/i18n/${navigator.language}`);

Это позволяет модулям использовать значения среды выполнения для вычисления путей зависимостей и может быть полезным для разделения разработка/продакшн код, интернационализации, разделения кода в зависимости от среды выполнения (браузер, Node.js) и т.д.

Инициализация ресурсов

const connection = await dbConnector()

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

Запасной вариант

В приведенном ниже примере показано, как глобальный await может использоваться для загрузки зависимости с реализацией запасного варианта. Если импорт из CDN A провалился, осуществляется импорт из CDN B:

let jQuery;try {  jQuery = await import('https://cdn-a.example.com/jQuery');} catch {  jQuery = await import('https://cdn-b.example.com/jQuery');}

Критика


Rich Harris составил список критических замечаний относительно await верхнего уровня. Он включает в себя следующее:

  • Глобальный await может блокировать выполнение кода
  • Глобальный await может блокировать получение ресурсов
  • Отсутствует поддержка CommonJS модулей

Вот какие ответы на эти замечания даются в FAQ предложения:

  • Поскольку дочерние узлы (модули) имеют возможность выполнения, блокировка кода, в конечном счете, отсутствует
  • Глобальный await используется на стадии выполнения графа модулей. На данном этапе все ресурсы получены и связаны, поэтому риска блокировки получения ресурсов не усматривается
  • await верхнего уровня ограничен ES6 модулями. Поддержка CommonJS модулей, как и обычных скриптов, изначально не планировалась

Я снова настоятельно рекомендую ознакомиться с FAQ предложения.

Надеюсь, мне удалось доступно объяснить суть рассматриваемого предложения. Собираетесь ли использовать эту возможность? Делитесь своим мнением в комментариях.
Подробнее..

Перевод Магические сигнатуры методов в C

01.07.2020 14:11:33 | Автор: admin

Представляю вашему вниманию перевод статьи The Magical Methods in C# автора CEZARY PITEK.


Есть определенный набор сигнатур методов в C#, имеющих поддержку на уровне языка. Методы с такими сигнатурами позволяют использовать специальный синтаксис со всеми его преимуществами. Например, с их помощью можно упростить наш код или создать DSL для того, чтобы выразить решение проблемы более красивым образом. Я встречаюсь с такими методами повсеместно, так что я решил написать пост и обобщить все мои находки по этой теме, а именно:


  • Синтаксис инициализации коллекций
  • Синтаксис инициализации словарей
  • Деконструкторы
  • Пользовательские awaitable типы
  • Паттерн query expression

Синтаксис инициализации коллекций


Синтаксис инициализации коллекции довольно старая фича, т. к. она существует с C# 3.0 (выпущен в конце 2007 года). Напомню, синтаксис инициализации коллекции позволяет создать список с элементами в одном блоке:


var list = new List<int> { 1, 2, 3 };

Этот код эквивалентен приведенному ниже:


var list = new List<int>();list.Add(1);list.Add(2);list.Add(3);

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


  • тип имплементирует интерфейс IEnumerable
  • тип имеет метод с сигнатурой void Add(T item)

public class CustomList<T>: IEnumerable{    public IEnumerator GetEnumerator() => throw new NotImplementedException();    public void Add(T item) => throw new NotImplementedException();}

Мы можем добавить поддержку синтаксиса инициализации коллекции, определив Add как метод расширения:


public static class ExistingTypeExtensions{    public static void Add<T>(ExistingType @this, T item) => throw new NotImplementedException();}

Этот синтаксис также можно использовать для вставки элементов в поле-коллекцию без публичного сеттера:


class CustomType{    public List<string> CollectionField { get; private set; }  = new List<string>();}class Program{    static void Main(string[] args)    {        var obj = new CustomType        {            CollectionField =            {                "item1",                "item2"            }        };    }}

Синтаксис инициализации коллекции полезен при инициализации коллекции известным числом элементов. Но что если мы хотим создать коллекцию с переменным числом элементов? Для этого есть менее известный синтаксис:


var obj = new CustomType{    CollectionField =    {        { existingItems }    }};

Такое возможно для типов, удовлетворяющих следующим условиям:


  • тип имплементирует интерфейс IEnumerable
  • тип имеет метод с сигнатурой void Add(IEnumerable<T> items)

public class CustomList<T>: IEnumerable{    public IEnumerator GetEnumerator() => throw new NotImplementedException();    public void Add(IEnumerable<T> items) => throw new NotImplementedException();}

К сожалению, массивы и коллекции из BCL не реализуют метод void Add(IEnumerable<T> items), но мы можем изменить это, определив метод расширения для существующих типов коллекций:


public static class ListExtensions{    public static void Add<T>(this List<T> @this, IEnumerable<T> items) => @this.AddRange(items);}

Благодаря этому мы можем написать следующее:


var obj = new CustomType{    CollectionField =    {        { existingItems.Where(x => /*Filter items*/).Select(x => /*Map items*/) }    }};

Или даже собрать коллекцию из смеси индивидуальных элементов и результатов нескольких перечислений (IEnumerable):


var obj = new CustomType{    CollectionField =    {        individualElement1,        individualElement2,        { list1.Where(x => /*Filter items*/).Select(x => /*Map items*/) },        { list2.Where(x => /*Filter items*/).Select(x => /*Map items*/) },    }};

Без подобного синтаксиса очень сложно получить подобный результат в блоке инициализации.


Я узнал об этой фиче совершенно случайно, когда работал с маппингами для типов с полями-коллекциями, сгенерированными из контрактов protobuf. Для тех, кто не знаком с protobuf: если вы используете grpctools для генерации типов .NET из файлов proto, все поля-коллекции генерируются подобным образом:


[DebuggerNonUserCode]public RepeatableField<ItemType> SomeCollectionField{    get    {        return this.someCollectionField_;    }}

Как можно заметить, поля-коллекции не имеют сеттер, но RepeatableField реализует метод void Add(IEnumerable items), так что мы по-прежнему можем инициализировать их в блоке инициализации:


/// <summary>/// Adds all of the specified values into this collection. This method is present to/// allow repeated fields to be constructed from queries within collection initializers./// Within non-collection-initializer code, consider using the equivalent <see cref="AddRange"/>/// method instead for clarity./// </summary>/// <param name="values">The values to add to this collection.</param>public void Add(IEnumerable<T> values){    AddRange(values);}

Синтаксис инициализации словарей


Одна из крутых фич C# 6.0 инициализация словаря по индексу, которая упростила синтаксис инициализации словарей. Благодаря ей мы можем писать более читаемый код:


var errorCodes = new Dictionary<int, string>{    [404] = "Page not Found",    [302] = "Page moved, but left a forwarding address.",    [500] = "The web server can't come out to play today."};

Этот код эквивалентен следующему:


var errorCodes = new Dictionary<int, string>();errorCodes[404] = "Page not Found";errorCodes[302] = "Page moved, but left a forwarding address.";errorCodes[500] = "The web server can't come out to play today.";

Это немного, но это определенно упрощает написание и чтение кода.


Лучшее в инициализации по индексу это то, что она не ограничивается классом Dictionary<T> и может быть использована с любым другим типом, определившим индексатор:


class HttpHeaders{    public string this[string key]    {        get => throw new NotImplementedException();        set => throw new NotImplementedException();    }}class Program{    static void Main(string[] args)    {        var headers = new HttpHeaders        {            ["access-control-allow-origin"] = "*",            ["cache-control"] = "max-age=315360000, public, immutable"        };    }}

Деконструкторы


В C# 7.0 помимо кортежей был добавлен механизм деконструкторов. Они позволяют декомпозировать кортеж в набор отдельных переменных:


var point = (5, 7);// decomposing tuple into separated variablesvar (x, y) = point;

Что эквивалентно следующему:


ValueTuple<int, int> point = new ValueTuple<int, int>(1, 4);int x = point.Item1;int y = point.Item2;

Этот синтаксис позволяет обменять значения двух переменных без явного объявления третьей:


int x = 5, y = 7;//switch(x, y) = (y,x);

Или использовать более краткий метод инициализации членов класса:


class Point{    public int X { get; }    public int Y { get; }    public Point(int x, int y)  => (X, Y) = (x, y);}

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


  • метод называется Deconstruct
  • метод возвращает void
  • все параметры метода имеют модификатор out

Для нашего типа Point мы можем объявить деконструктор следующим образом:


class Point{    public int X { get; }    public int Y { get; }    public Point(int x, int y) => (X, Y) = (x, y);    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);}

Пример использования приведен ниже:


var point = new Point(2, 4);var (x, y) = point;

"Под капотом" он превращается в следующее:


int x;int y;new Point(2, 4).Deconstruct(out x, out y);

Деконструкторы могут быть добавлены к типам с помощью методов расширения:


public static class PointExtensions{     public static void Deconstruct(this Point @this, out int x, out int y) => (x, y) = (@this.X, @this.Y);}

Один из самых полезных примеров применения деконструкторов это деконструкция KeyValuePair<TKey, TValue>, которая позволяет с легкостью получить доступ к ключу и значению во время итерирования по словарю:


foreach (var (key, value) in new Dictionary<int, string> { [1] = "val1", [2] = "val2" }){    //TODO: Do something}

KeyValuePair<TKey, TValue>.Deconstruct(TKey, TValue) доступно только с netstandard2.1. Для предыдущих версий netstandard нам нужно использовать ранее приведенный метод расширения.


Пользовательские awaitable типы


C# 5.0 (выпущен вместе с Visual Studio 2012) ввел механизм async/await, который стал переворотом в области асинхронного программирования. Прежде вызов асинхронного метода представлял собой запутанный код, особенно когда таких вызовов было несколько:


void DoSomething(){    DoSomethingAsync().ContinueWith((task1) => {        if (task1.IsCompletedSuccessfully)        {            DoSomethingElse1Async(task1.Result).ContinueWith((task2) => {                if (task2.IsCompletedSuccessfully)                {                    DoSomethingElse2Async(task2.Result).ContinueWith((task3) => {                        //TODO: Do something                    });                }            });        }    });}private Task<int> DoSomethingAsync() => throw new NotImplementedException();private Task<int> DoSomethingElse1Async(int i) => throw new NotImplementedException();private Task<int> DoSomethingElse2Async(int i) => throw new NotImplementedException();

Это может быть переписано намного красивее с использованием синтаксиса async/await:


async Task DoSomething(){    var res1 = await DoSomethingAsync();    var res2 = await DoSomethingElse1Async(res1);    await DoSomethingElse2Async(res2);}

Это может прозвучать удивительно, но ключевое слово await не зарезервировано только под использование с типом Task. Оно может быть использовано с любым типом, который имеет метод GetAwaiter, возвращающий удовлетворяющий следующим требованиям тип:


  • тип имплементирует интерфейс System.Runtime.CompilerServices.INotifyCompletion и реализует метод void OnCompleted(Action continuation)
  • тип имеет свойство IsCompleted логического типа
  • тип имеет метод GetResult без параметров

Для добавления поддержки ключевого слова await к пользовательскому типу мы должны определить метод GetAwaiter, возвращающий TaskAwaiter<TResult> или пользовательский тип, удовлетворяющий приведенным выше условиям:


class CustomAwaitable{    public CustomAwaiter GetAwaiter() => throw new NotImplementedException();}class CustomAwaiter: INotifyCompletion{    public void OnCompleted(Action continuation) => throw new NotImplementedException();    public bool IsCompleted => throw new NotImplementedException();    public void GetResult() => throw new NotImplementedException();}

Вы можете спросить: "Каков возможный сценарий использования синтаксиса await с пользовательским awaitable типом?". Если это так, то я рекомендую вам прочитать статью Stephen Toub под названием "await anything", которая показывает множество интересных примеров.


Паттерн query expression


Лучшее нововведение C# 3.0 Language-Integrated Query, также известное как LINQ, предназначенное для манипулирования коллекциями с SQL-подобным синтаксисом. LINQ имеет две вариации: SQL-подобный синтаксис и синтаксис методов расширения. Я предпочитаю второй вариант, т. к. по моему мнению он более читаем, а также потому что я привык к нему. Интересный факт о LINQ заключается в том, что SQL-подобный синтаксис во время компиляции транслируется в синтаксис методов расширения, т. к. это фича C#, а не CLR. LINQ был разработан в первую очередь для работы с типами IEnumerable, IEnumerable<T> и IQuerable<T>, но он не ограничен только ими, и мы можем использовать его с любым типом, удовлетворяющим требованиям паттерна query expression. Полный набор сигнатур методов, используемых LINQ, таков:


class C{    public C<T> Cast<T>();}class C<T> : C{    public C<T> Where(Func<T,bool> predicate);    public C<U> Select<U>(Func<T,U> selector);    public C<V> SelectMany<U,V>(Func<T,C<U>> selector, Func<T,U,V> resultSelector);    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,U,V> resultSelector);    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector);    public O<T> OrderBy<K>(Func<T,K> keySelector);    public O<T> OrderByDescending<K>(Func<T,K> keySelector);    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector);    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector, Func<T,E> elementSelector);}class O<T> : C<T>{    public O<T> ThenBy<K>(Func<T,K> keySelector);    public O<T> ThenByDescending<K>(Func<T,K> keySelector);}class G<K,T> : C<T>{    public K Key { get; }}

Разумеется, мы не обязаны реализовывать все эти методы для того, чтобы использовать синтаксис LINQ с нашим пользовательским типом. Список обязательных операторов и методов LINQ для них можно посмотреть здесь. Действительно хорошее объяснение того, как это сделать, можно найти в статье Understand monads with LINQ автора Miosz Piechocki.


Подведение итогов


Цель этой статьи заключается вовсе не в том, чтобы убедить вас злоупотреблять этими синтаксическими трюками, а в том, чтобы сделать их более понятными. С другой стороны, их нельзя всегда избегать. Они были разработаны для того, чтобы их использовать, и иногда они могут сделать ваш код лучше. Если вы боитесь, что получившийся код будет непонятен вашим коллегам, вам нужно найти способ поделиться знаниями с ними (или хотя бы ссылкой на эту статью). Я не уверен, что это полный набор таких "магических методов", так что если вы знаете еще какие-то пожалуйста, поделитесь в комментариях.

Подробнее..

Работа с асинхронностью в Dart

27.01.2021 14:17:26 | Автор: admin

Всем привет! Меня зовут Дмитрий Репин, я Flutter-разработчик в Surf.

В этой статье я расскажу, как работать с асинхронностью в Dart: всё о самых важных классах библиотеки dart:async с примерами под катом. Поговорим о том, как в однопоточном языке сходить в сеть или базу данных и при этом не затормозить приложение.

Эта статья написана по материалам моего ролика на YouTube. Посмотрите видео, если больше любите слушать, чем читать.

Dart однопоточный язык программирования, который выполняется в одном процессе. Что это значит по факту: если мы будем в одном потоке выполнять любую операцию, требующую времени, то приложение просто подвиснет. И всё время, пока мы, например, ждём ответа от сервера или выполнения запроса в БД, пользователь будет страдать и смотреть на лагающий интерфейс.

К счастью, Dart хитрый однопоточный язык, который предоставляет механизм Event Loop он даёт возможность откладывать какие-то операции на потом, когда поток будет посвободнее.Такие отложенные операции мы будем называть асинхронными.

Все операции в Dart можно разделить на два типа:

  1. Синхронные те, что блокируют другие операции до своего выполнения.

  2. Асинхронные, которые позволяют другим операциям выполняться, пока текущая не закончится.

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

Создание асинхронной функции с помощью класса Future

Чтобы было нагляднее, давайте напишем простую асинхронную функцию. Пусть она будет принимать строку и смотреть на её длину: если она больше 10 символов, функция вернёт число 42, в ином случае ошибку. Представим, что вычисление длины строки достаточно тяжеловесная операция чтобы сделать её асинхронной.

Чтобы сделать код асинхронным, нам понадобится класс Future. В конструктор он принимает функцию, которую необходимо выполнить асинхронно, а также предоставляет два обработчика: then и catchError. Первый обрабатывает успешное выполнение функции, а второй выполнение функции с ошибкой. В итоге у нас должен получиться такой код:

runSimpleFutureExample({    String question = 'В чем смысл жизни и всего такого?',  }) {    print('Start of future example');    Future(() {      print('Вопрос: $question');      if (question.length > 10) {         return 42;      } else {        throw Exception('Вы задали недостаточно сложный вопрос.');      }    }).then((result) {      print('Ответ: $result');    }).catchError((error) {      print('Ошибка. $error');    });    print('Finish of future example');  }

В начале и конце метода выводятся строки о том, что он начал и закончил работу, а между ними создаётся сам future. В конструктор он принимает метод, который вычисляет длину строки, а затем к Future добавляются обработчики then и catchError, которые обрабатывают выполнение Future. Попробуем запустить этот пример и посмотрим, в каком порядке выводится на консоль результат:

Start of future example

Finish of future example

Вопрос: В чем смысл жизни и всего такого?

Ответ: 42

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

Работать с Future через обработчики then и catchError не самая хорошая идея, особенно если нужно передать результат одного Future во второй, а второго в третий и т. д., порождая callback hell.

Для таких случаев Dart предоставляет ключевые слова async и await. Словом async помечается функция, исполняющая асинхронные операции через await. Словом await помечаются сами асинхронные операции. К сожалению, при таком подходе мы теряем обработчик catchError, однако никто не мешает нам обрабатывать ошибки через стандартный try/catch. Чтобы понять, как это работает, перепишем наш предыдущий пример с использованием async/await и для удобства вынесем Future в отдельный метод:

 runFutureWithAwaitExample({    String question = 'В чем смысл жизни и всего такого?',  }) async {    print('Start of future example');    try {      final result = await _getAnswerForQuestion(        'В чем смысл жизни и всего такого?',      );      print('Ответ: $result');    } catch (error) {      print('Ошибка. $error');    }    print('Finish of future example');  }   Future<int> _getAnswerForQuestion(String question) => Future(() {        print('Вопрос: $question');        if (question.length > 10) {          return 42;        } else {          throw Exception('Вы задали недостаточно сложный вопрос.');        }      });

Здесь мы объявили метод, возвращающий Future. Вызываем его с использованием слова await, передавая в переменную result, с которой мы можем работать дальше, как будто в синхронном коде. Выведем на консоль результат работы метода:

Start of future example

Вопрос: В чем смысл жизни и всего такого?

Ответ: 42

Finish of future example

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

Обработка последовательности асинхронных событий с помощью Stream

Помимо Future, есть ещё один важный класс для работы с асинхронностью Stream. В отличие от Future, Stream может обрабатывать целый набор асинхронных событий. Давайте сразу разберём на примере:

void runStreamSimpleExample() {    print('Simple stream example started');    final stream = Stream.fromIterable([1, 2, 3, 4, 5]);    stream.listen((number) {      print('listener: $number');    });    print('Simple stream example finished');  }

У стрима много разных конструкторов, посмотрите их в документации. Здесь используем fromIterable. Создаём в массиве пять чисел, подписываемся на стрим через метод listen и выводим на консоль:

Simple stream example started

Simple stream example finished

listener: 1

listener: 2

listener: 3

listener: 4

listener: 5

Как и в случае с Future, сначала выполняется синхронный код: сообщения о старте и завершения примера. Только потом обрабатывается стрим и выводятся на консоль числа. В этом примере подписка на стрим обрабатывается асинхронно через listen, однако есть способ обрабатывать стрим через знакомые нам ключевые слова async и await. Перепишем пример с их использованием:

void runStreamAwaitedSimpleExample() async {    print('Simple stream example with await started');    final stream = Stream.fromIterable([1, 2, 3, 4, 5]);    await for (final number in stream) {      print('Number: $number');    }    print('Simple stream example with await finished');  }

Теперь обработка стрима похожа на обычный цикл. Но не стоит обрабатывать слишком большие стримы таким образом: это может затормозить работу программы. Выведем результат на консоль:

Simple stream example started

listener: 1

listener: 2

listener: 3

listener: 4

listener: 5

Simple stream example finished

Тут всё так же, как и с async/await во future. Сама функция стала асинхронной, поэтому теперь обработка Stream происходит до завершения метода.

Single-subscription и broadcast стримы. Основы StreamController

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

print('Simple stream example started');    final stream = Stream.fromIterable([1, 2, 3, 4, 5]);    stream.listen((number) {      print('listener 1: $number');    });    stream.listen((number) {      print('listener 2: $number');    });    print('Simple stream example finished');

И результат:

The following StateError was thrown while handling a gesture.

Bad state: Stream has already been listened to.

Мы получаем ошибку: на стрим уже произведена подписка и подписаться второй раз не выйдет.

Здесь стоит рассказать о том, что существует два типа стримов: single-subscription и broadcast стримы. Мы создали single-subscription стрим. Такие стримы поставляют все данные подписчику разом и только после самой подписки. Так и происходит в нашем примере.

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

Чтобы увидеть разницу, создадим broadcast стрим. В этом примере для создания стрима мы не будем использовать непосредственно сам Stream. Мы будем работать с классом StreamController. Он предоставляет доступ к самому стриму, даёт возможность управлять им и добавлять в него события.

///пример broadcast стрима  void runBroadcastStreamExample() {    print('Broadcast stream example started');    final streamController = StreamController.broadcast();    streamController.stream.listen((number) {      print('Listener 1: $number');    });    streamController.stream.listen((number) {      print('Listener 2: $number');    });    streamController.sink.add(1);    streamController.sink.add(2);    streamController.sink.add(3);    streamController.sink.add(4);    streamController.sink.add(5);    streamController.close();    print('Broadcast stream example finished');  }

Мы создали StreamController с broadcast стримом, дважды подписались на него и добавили через sink пять чисел, после чего закрыли стрим. Смотрим, что ушло на консоль:

Broadcast stream example started

Broadcast stream example finished

Listener 1: 1

Listener 2: 1

Listener 1: 2

Listener 2: 2

Listener 1: 3

Listener 2: 3

Listener 1: 4

Listener 2: 4

Listener 1: 5

Listener 2: 5

Проблем не возникло: два подписчика отработали, как и ожидалось. На самом деле у StreamController достаточно много функций, рекомендую почитать документацию к нему.

Отписка от стрима с помощью StreamSubscription

Помимо управления самим стримом часто возникает потребность управлять подпиской на него для этого в dart:async добавлен StreamSubscription. Кстати, мы уже неявно работали с ним в наших предыдущих примерах, когда подписывались на стрим: на самом деле метод listen возвращает его, и таким образом мы можем выделить подписку в переменную. Модифицируем предыдущий пример так, чтобы вторая подписка отменялась после числа 3:

///пример broadcast стрима  void runBroadcastStreamExample() {    print('Broadcast stream example started');    final streamController = StreamController.broadcast();    streamController.stream.listen((number) {      print('Listener 1: $number');    });    StreamSubscription sub2;    sub2 = streamController.stream.listen((number) {      print('Listener 2: $number');      if (number == 3) {        sub2.cancel();      }    });    streamController.sink.add(1);    streamController.sink.add(2);    streamController.sink.add(3);    streamController.sink.add(4);    streamController.sink.add(5);    streamController.close();    print('Broadcast stream example finished');  }

Тут без сюрпризов, на консоль выводится то, что мы ожидали:

Broadcast stream example started

Broadcast stream example finished

Listener 1: 1

Listener 2: 1

Listener 1: 2

Listener 2: 2

Listener 1: 3

Listener 2: 3

Listener 1: 4

Listener 1: 5

Стоит отметить, что StreamSubscription не только даёт возможность отменять подписку полностью, но и приостанавливать и возобновлять её, а также предоставляет колбэки на события подписки.

Как упростить работу с асинхронностью

Мы разобрали все самые важные классы для dart:async, но осталось ещё три, о которых я бы хотел рассказать. Возможно, они используются не так часто, но могут быть очень полезны в определённых ситуациях.

Completer

Completer позволяет поставлять Future, отправлять событие о выполнении или событие об ошибке. Это может быть полезно, когда нужно сделать цепочку Future и вернуть результат.

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

class CompleterTester {  void runCompleterInitTest() async {    print('Completer example started');    var sumCompleter = SumCompleter();    var sum = await sumCompleter.sum(20, 22);    print('Completer result: ' + sum.toString());    print('Completer example finished');  }} class SumCompleter {  Completer<int> completer = Completer();   Future<int> sum(int a, int b) {    _sumAsync(a, b);    return completer.future;  }   void _sumAsync(int a, int b) {    Future.delayed(Duration(seconds: 3), () {      return a + b;    }).then((value) {      completer.complete(value);    });  }}

Разберём, что тут произошло. Мы создали класс SumCompleter и создали в нём completer. Затем объявили метод sum, который вызывает приватный метод на сложение и возвращает Future этого completer. Затем этот приватный метод выполняет операцию и вызывает метод complete у комплитера. После этого пользователь метода получает результат.

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

StreamIterator

Представьте, что вам нужно управлять переходом к следующему айтему стрима и делать это именно тогда, когда вам нужно. Именно это и делает StreamIterator. Он предоставляет метод moveNext для перехода к следующему элементу стрима. moveNext вернет true, если элемент пришел, и false, если стрим был закрыт. Также StreamIterator предоставляет свойство current для получения текущего элемента. Ну и конечно, метод cancel для отмены подписки. Небольшой пример по работе класса:

void runStreamIteratorExample() async {    print('StreamIteratorExample started');    var stream = Stream.fromIterable([1, 2, 3]);    var iterator = StreamIterator(stream);    bool moveResult;    do {      moveResult = await iterator.moveNext();      print('number: ${iterator.current}');    } while (moveResult);    print('StreamIteratorExample finished');  }

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

StreamTransformer

На самом деле это немного переусложнённая штука, но она отлично справляется, если вам нужно много раз трансформировать поток в приложении определённым образом. Самый очевидный пример разделение файла на строки. Разберём на примере и напишем трансформер, который будет удваивать приходящее в него число:

void runStreamTransformerExample() async {    print('StreamTransformer example started');    StreamTransformer doubleTransformer =        new StreamTransformer.fromHandlers(handleData: (data, EventSink sink) {      sink.add(data * 2);    });     StreamController controller = StreamController();    controller.stream.transform(doubleTransformer).listen((data) {      print('data: $data');    });     controller.add(1);    controller.add(2);    controller.add(3);    print('StreamTransformer example finished');  }

В нашем случае он будет принимать на вход данные, которые нужно трансформировать, и sink, в который мы должны передать трансформированные данные. В теле метода просто удваиваем приходящее значение. Затем остаётся только трансформировать стрим с помощью метода transform. И вуаля: в методе listen значение удваивается. Обратите внимание: трансформер может и не отдать данные в синк, а может отдать два раза. То есть он работает не только как map или where, а наделён гораздо большей функциональностью.


Рекомендую поиграться с этими примерами они есть на нашем GitHub. Так вы точно поймете, как устроена асинхронность в Dart. Если что-то непонятно, задавайте вопросы я отвечу в комментариях.

Подробнее..

Перевод Почему в Visual Studio стек вызовов асинхронного кода иногда перевёрнут?

03.05.2021 18:11:31 | Автор: admin

Вместе с моим коллегой Евгением мы потратили много времени. Приложение обрабатывает тысячи запросов в асинхронном конвейере, полном async/await. Во время нашего исследования мы получили странные вызовы, они выглядели как бы перевернутыми. Цель этого поста рассказать, почему вызовы могут оказаться перевёрнутыми даже в Visual Studio.


Давайте посмотрим результат профилирования в Visual Studio

Я написал простое приложение .NET Core, которое имитирует несколько вызовов async/await:

static async Task Main(string[] args){    Console.WriteLine($"pid = {Process.GetCurrentProcess().Id}");    Console.WriteLine("press ENTER to start...");    Console.ReadLine();    await ComputeAsync();    Console.WriteLine("press ENTER to exit...");    Console.ReadLine();}private static async Task ComputeAsync(){    await Task.WhenAll(        Compute1(),        ...        Compute1()        ); }

ComputeAsync запускает много задач, выполнения которых будут ожидать другие методы async:

private static async Task Compute1(){    ConsumeCPU();    await Compute2();    ConsumeCPUAfterCompute2();}private static async Task Compute2(){    ConsumeCPU();    await Compute3();    ConsumeCPUAfterCompute3();}private static async Task Compute3(){    await Task.Delay(1000);    ConsumeCPUinCompute3();    Console.WriteLine("DONE");}

В отличие от методов Compute1 и Compute2, последний Compute3 ждёт одну секунду, прежде чем задействовать какие-то ресурсы CPU и вычислить квадратный корень в хелперах CompusumeCPUXXX:

[MethodImpl(MethodImplOptions.NoInlining)]private static void ConsumeCPUinCompute3(){    ConsumeCPU();}[MethodImpl(MethodImplOptions.NoInlining)]private static void ConsumeCPUAfterCompute3(){    ConsumeCPU();}[MethodImpl(MethodImplOptions.NoInlining)]private static void ConsumeCPUAfterCompute2(){    ConsumeCPU();}private static void ConsumeCPU(){    for (int i = 0; i < 1000; i++)        for (int j = 0; j < 1000000; j++)        {            Math.Sqrt((double)j);        }}

В Visual Studio использование ЦП этой тестовой программой профилируется через меню Debug | Performance Profiler....

На панели итоговых результатов (Summary result) нажмите ссылку Open Details....

И выберите древовидное представление стека вызовов.

Вы должны увидеть два пути выполнения:

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

...если методы были синхронными, что не соответствует действительности. Таким образом, чтобы представить красивый стек вызовов, Visual Studio проделала отличную работу с деталями реализации async/await. Однако если вы откроете первый узел, то получите нечто более тревожное:

... если вы не знаете, как реализованы async/await. Мой код Compute3 определённо не вызывает Compute2, который не вызывает Compute1! Именно здесь реконструкция интеллектуального фрейма/стека вызовов Visual Studio вносит самую большую путаницу. Что же происходит?

Разбираемся с реализацией async/await

Visual Studio скрывает реальные вызовы, но с помощью dotnet-dump и команды pstacks вы сможете увидеть, какие методы на самом деле вызываются:

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

  1. Обратный вызов таймера вызывает <Compute3>d__4.MoveNext(), что соответствует концу Task.Delay в методе Compute3.

  2. <Compute2>d__3.MoveNext() вызывается после await Compute3, чтобы продолжить выполнение кода.

  3. <Compute1>d__.MoveNext() вызывается после await Compute2.

  4. ConsumeCPUAfterCompute2() вызывается как ожидалось.

  5. ComputeCPU() или ConsumeCPUInCompute3() также вызываются как ожидалось.

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

Все эти типы <имя метода>d__* содержат поля, соответствующие каждому асинхронному методу локальных переменных и параметров, если таковые имеются. Например, вот что генерируется для методов ComputeAsync и Compute1/2/3 async без локальных переменных или параметров:

Поле integer <>1__state отслеживает состояние выполнения машины. К примеру, после создания конечного автомата в Compute1 этому полю присваивается значение -1:

Я не хочу углубляться в детали конструктора, но давайте просто скажем, что метод MoveNext машины состояний <Compute1>d__2 выполняется тем же потоком. Прежде чем рассматривать соответствующую методу Compute1 реализацию MoveNext (без обработки исключений), имейте в виду, что она должна:

  1. Выполнить весь код до вызова await.

  2. Изменить состояние исполнения (об этом позже).

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

  4. Вернуться, чтобы продолжить выполнение кода после вызова await.

  5. И делать это до следующего вызова await, снова и снова.

<>1__state равно -1, поэтому выполняется первая "синхронная" часть кода, то есть вызывается метод ComsumeCPU).

Затем для получения соответствующего ожидаемого объекта вызывается метод Compute2 (здесь Task). Если задача выполняется немедленно (т.е. нет вызова await, такого как простая задача . FromResult() в методе async), IsCompleted() вернёт true, и код после вызова await будет выполняться тем же потоком. Да, это означает, что вызовы async/await могут выполняться синхронно одним и тем же потоком: зачем создавать поток, когда он не нужен?

Если задача передаётся в пул потоков для выполнения потоком-воркером, значение <>1__state устанавливается в 0 (поэтому при следующем вызове MoveNext будет выполнена следующая синхронная часть (т.е. после вызова await).

Теперь код вызывает awaitUnsafeOnCompleted, чтобы сотворить магию: добавить продолжение к задаче Compute2 (первый параметр awaiter), чтобы MoveNext был вызван на той же машине состояния (второй параметр this), когда задача завершится. Затем тихо возвращается текущий поток.

Поэтому, когда заканчивается задача Compute2, её продолжение выполняется для вызова MoveNext, на этот раз с <>1__state в значении 0, поэтому выполняются последние две строки: awaiter.GetResult() возвращается немедленно, потому что возвращённая Compute2 Task уже завершена, и теперь вызывается последний метод CinsumeCPUAfterCompute2. Вот краткое описание происходящего:

  • Каждый раз, когда вы видите асинхронный метод, с помощью метода MoveNext компилятор C# генерирует выделенный тип конечного автомата, отвечающий за синхронное выполнение кода между вызовами await.

  • Каждый раз, когда вы видите вызов await, это означает, что продолжение добавится к задаче Task, обёртывающей выполняемый метод async. Этот код продолжения вызовет метод MoveNext конечного автомата вызывающего метода, чтобы выполнить следующий фрагмент кода, до следующего вызова await.

Вот почему Visual Studio, пытаясь точно сопоставить каждый фрейм состояния асинхронного метода MoveNext исходя из самого метода, показывает перевёрнутые стеки вызовов: фреймы соответствуют продолжениям после вызовов await (зелёным цветом на предыдущем рисунке).

Обратите внимание, что я более подробно описал, как работает async/await, а также действие AwaitUnsageOnCompleted во время сессии на конференции DotNext с Кевином, поэтому не стесняйтесь смотреть запись с этого момента, если вы хотите углубиться в тему.

А если вы хотите погрузиться в C# обратите внимание на наш курс по разработке на этом языке. Универсальный стек среди ваших навыков серьёзно укрепит ваши позиции на рынке труда и увеличит доход.

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

Другие профессии и курсы
Подробнее..

Из песочницы Как я понимаю асинхронный код?

01.08.2020 18:14:38 | Автор: admin
Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи How Do I Think About Async Code?! автора Leslie Richardson.

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

Что такое асинхронный код?


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

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

Почему мне стоит использовать асинхронный код? Пример, пожалуйста!


Чтобы иметь какую-то аналогию для демонстрации асинхронного программирования, рассмотрим процесс выпечки пирога. Этот процесс будет представлен потоком, который выполняет несколько шагов (или задач), как показано в коде ниже. Этот код корректен, и у вас все равно получится вкусный пирог после выполнения метода. Однако, поскольку весь код является синхронным, каждая строка будет выполняться последовательно. Другими словами, вы будете стоять совершенно неподвижно, ожидая, пока печь завершит предварительный нагрев. А ведь в это же самое время вы могли бы сделать тесто для вашего пирога!

Синхронный метод MakeCake()

image

Синхронная программа выпекания пирога

image

В реальной жизни вы, как правило, разделяете этот процесс на задачи, замешиваете тесто, пока духовка разогревается. Или делаете глазурь, в то время как пирог запекается в духовке. Это увеличивает вашу производительность и позволяет испечь торт намного быстрее. Это как раз тот случай, где асинхронный код пригодится! Сделав наш текущий код асинхронным, мы сможем заняться другими делами, чтобы скоротать время, в то время пока мы ожидаем(await) результата задачи(task), такой как выпекание пирога в духовке.
Чтобы сделать это изменим наш код, а так же добавим метод PassTheTime. Теперь наш код сохраняет состояние задачи, запускает другую синхронную или асинхронную операцию и получает результат сохраненной задачи, в тот момент, когда это необходимо.

Асинхронный метод MakeCake()
image

Асинхронная программа выпечки пирога
image

По сравнению с синхронным методом MakeCake, в котором отсутствует метод PassTheTime, асинхронному методу MakeCakeAsync удается выполнить больше задач, не блокируя поток, что сокращает время, необходимое для выполнения всего метода в целом.

Сравнение асинхронной и синхронной программ

image

Как мне писать асинхронный код в .NET?


C # позволяет писать асинхронный код, используя тип Task и ключевые слова await и async. Тип Task сообщает вызывающей стороне о возможном типе возвращаемого значения. Он также указывает на то, что другие действия могут продолжать выполняться в вызвавшем его методе. Ключевое слово async работает в паре с ключевым словом await, которое уведомляет компилятор о том, что нам потребуется возвращаемое методом значение, но не сразу. В результате нам не нужно блокировать вызывающий поток, и мы можем продолжать выполнение других задач, пока не потребуется ожидаемое значение. Первоначально асинхронный метод будет выполняться синхронно, пока не будет найдено ключевое слово await. Это именно тот момент, когда выполнение метода начнется асинхронно.

Я узнал об асинхронном коде! Что теперь?


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

Приложения, использующие HTTP-запросы в зависимости от запроса, обработка HTTP-вызовов может занять длительное время. Использование асинхронного кода позволяет нам выполнить другие операции, пока мы ожидаем ответа от сервера.

Пример запроса HTTP GET

image

Приложения, с пользовательским интерфейсом приложения WPF или любые другие, использующие кнопки, текстовые поля и другие ресурсы UX, также отлично подходят для асинхронной реализации. Например, приложение WPF, производящее анализ файла. Данная процедура может занять некоторое время. Однако, сделав это действие асинхронным, вы по-прежнему сможете взаимодействовать с пользовательским интерфейсом, не останавливая приложение полностью, во время ожидания завершения операции.
Подробнее..
Категории: C , Net , Async/await

Реализуем кооперативную многозадачность на C

07.03.2021 00:21:34 | Автор: admin


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


Вот наша простенькая заготовка:


static void Main(){    DoWork("A", 4);    DoWork("B", 3);    DoWork("C", 2);    DoWork("D", 1);}static void DoWork(string name, int num){    for (int i = 1; i <= num; i++)    {        Console.WriteLine($"Work {name}: {i}");    }    Console.WriteLine($"Work {name} is completed");}

Статический метод вызывается последовательно 4 раза и выводит символы A, B, C, D заданное количество раз. Символы, очевидно, также выводятся последовательно, и наша задача здесь добиться того, что бы они выводились попеременно, но при этом сильно не меняя исходный код и не используя дополнительные потоки.


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


Вопрос: "Какая конструкция C# позволяет прервать работу метода на какое-то заранее неизвестное время?" Правильно! await! Давайте сделаем наш метод асинхронным и добавим await после вывода в консоль очередного символа:


static async ValueTask DoWork(string name, int num){    for (int i = 1; i <= num; i++)    {        Console.WriteLine($"Work {name}: {i}");        await /*Something*/    }    Console.WriteLine($"Work {name} is completed");}

Этот await обозначает, что метод решил прерваться, чтобы другие вызовы тоже смогли вывести свои символы. Но что именно этот метод будет await-ить? Task.Delay(), Task.Yield() не подходят, так как они подразумевают переключение на другие потоки. Тогда создадим свой класс, который можно использовать с await, и который не будет иметь ничего общего с многопоточкой. Назовем его CooperativeBroker:


private class CooperativeBroker : ICooperativeBroker{    private Action? _continuation;    public void GetResult()         => this._continuation = null;    public bool IsCompleted         => false;//Preventing sync completion in async method state machine    public void OnCompleted(Action continuation)    {        this._continuation = continuation;        this.InvokeContinuation();    }    public ICooperativeBroker GetAwaiter()         => this;    public void InvokeContinuation()         => this._continuation?.Invoke();}

Компилятор C# преобразует исходный код асинхронных методов в виде конечного автомата, каждое состояние которого соответствует вызову await внутри этого метода. Код перехода в следующее состояние передается в виде делегата continuation в метод OnCompleted. В реальной жизни предполагается, что continuation будет вызван, когда будет завершена асинхронная операция, но в нашем случае никаких асинхронных операций нет и для работы программы надо бы было вызвать это continuation немедленно, но тогда методы опять будут работать последовательно, а мы этого не хотим. Лучше сохраним этот делегат на будущее и дадим поработать другим вызовам. Чтобы было где хранить делегаты давайте добавим класс CooperativeContext:


private class CooperativeBroker{    private readonly CooperativeContext _cooperativeContext;    private Action? _continuation;    public CooperativeBroker(CooperativeContext cooperativeContext)        => this._cooperativeContext = cooperativeContext;    ...    public void OnCompleted(Action continuation)    {        this._continuation = continuation;        this._cooperativeContext.OnCompleted(this);    }}public class CooperativeContext{    private readonly List<CooperativeBroker> _brokers =         new List<CooperativeBroker>();    void OnCompleted(CooperativeBroker broker)    {        ...    }}

где метод OnCompleted собственно и будет отвечать за поочередный вызов методов:


private void OnCompleted(CooperativeBroker broker){    //Пропускает вызовы делегатов пока все брокеры не добавлены.    if (this._targetBrokersCount == this._brokers.Count)    {        var nextIndex = this._brokers.IndexOf(broker) + 1;        if (nextIndex == this._brokers.Count)        {            nextIndex = 0;        }        this._brokers[nextIndex].InvokeContinuation();    }}

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


Так как нам нужно заранее знать общее количество кооперативных вызовов, то перепишем эти вызовы следующим образом:


static void Main(){    CooperativeContext.Run(        b => DoWork(b, "A", 4),        b => DoWork(b, "B", 3),        b => DoWork(b, "C", 2),        b => DoWork(b, "D", 1)    );}static async ValueTask DoWork(CooperativeBroker broker, string name, int num, bool extraWork = false){    for (int i = 1; i <= num; i++)    {        Console.WriteLine($"Work {name}: {i}, Thread: {Thread.CurrentThread.ManagedThreadId}");        await broker;    }    Console.WriteLine($"Work {name} is completed, Thread: {Thread.CurrentThread.ManagedThreadId}");}public class CooperativeContext{    public static void Run(params Func<CooperativeBroker, ValueTask>[] tasks)    {        CooperativeContext context = new CooperativeContext(tasks.Length);        foreach (var task in tasks)        {            task(context.CreateBroker());        }        ...    }    ...    private int _targetBrokersCount;    private CooperativeContext(int maxCooperation)    {        this._threadId = Thread.CurrentThread.ManagedThreadId;        this._targetBrokersCount = maxCooperation;    }    ...}

Вроде бы можно уже и запускать, чтобы проверить как это работает, но осталась еще одна маленькая деталь если один из вызовов завершается раньше других, то он перестает вызывать метод OnCompleted и вся наша цепочка прерывается. Что бы исправить эту ситуацию форсируем вызов оставшихся "продолжений", и заодно удалим ненужный уже брокер:


public class CooperativeContext{    public static void Run(params Func<ICooperativeBroker, ValueTask>[] tasks)    {        CooperativeContext context = new CooperativeContext(tasks.Length);        foreach (var task in tasks)        {            task(context.CreateBroker());        }        // Программа приходит сюда когда один из методов завершен, но надо        // закончить и остальные        while (context._brokers.Count > 0)        {            context.ReleaseFirstFinishedBrokerAndInvokeNext();        }    }    ...    private void ReleaseFirstFinishedBrokerAndInvokeNext()    {        // IsNoAction означает что асинхронный метод завершен        var completedBroker = this._brokers.Find(i => i.IsNoAction)!;        var index = this._brokers.IndexOf(completedBroker);        this._brokers.RemoveAt(index);        this._targetBrokersCount--;        if (index == this._brokers.Count)        {            index = 0;        }        if (this._brokers.Count > 0)        {            this._brokers[index].InvokeContinuation();        }    }    }private class CooperativeBroker : ICooperativeBroker{    ...    public bool IsNoAction        => this._continuation == null;    ...}

Вот теперь можно и запускать (чуть усложним нам задачу введя дополнительную работу):


static void Main(){    CooperativeContext.Run(        b => DoWork(b, "A", 4),        b => DoWork(b, "B", 3, extraWork: true),        b => DoWork(b, "C", 2),        b => DoWork(b, "D", 1)    );}static async ValueTask DoWork(    ICooperativeBroker broker,     string name,     int num,     bool extraWork = false){    for (int i = 1; i <= num; i++)    {        Console.WriteLine(               $"Work {name}: {i}, Thread: {Thread.CurrentThread.ManagedThreadId}");        await broker;        if (extraWork)        {            Console.WriteLine(                   $"Work {name}: {i} (Extra), Thread: {Thread.CurrentThread.ManagedThreadId}");            await broker;        }    }    Console.WriteLine(           $"Work {name} is completed, Thread: {Thread.CurrentThread.ManagedThreadId}");}

Результат:


Work A: 1, Thread: 1Work B: 1, Thread: 1Work C: 1, Thread: 1Work D: 1, Thread: 1Work A: 2, Thread: 1Work B: 1 (Extra), Thread: 1Work C: 2, Thread: 1Work D is completed, Thread: 1Work A: 3, Thread: 1Work B: 2, Thread: 1Work C is completed, Thread: 1Work A: 4, Thread: 1Work B: 2 (Extra), Thread: 1Work A is completed, Thread: 1Work B: 3, Thread: 1Work B: 3 (Extra), Thread: 1Work B is completed, Thread: 1

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




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


Исходный код вы можете найти на github.


Если вышеприведенный код вам все же кажется вам какой-то чертовщиной, то могу порекомендовать послушать мой доклад на CLRium про асинхронную машину состояний в C#. Там подробно разобрано как работает async/await.

Подробнее..
Категории: C , Net , Async/await , Multithreading

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru