# C# Параллельное программирование и асинхронность

Общие принципы асинхронности мы проходили в прошлом году. немного повторим и рассмотрим подробнее работу в C#.

Асинхронность в C# работает с **задачами**, поэтому сначала разберёмся с ними

## Задачи и класс Task

<!-- https://metanit.com/sharp/tutorial/12.1.php -->

В эпоху многоядерных машин, которые позволяют параллельно выполнять сразу несколько процессов, стандартных средств работы с потоками в .NET уже оказалось недостаточно. Поэтому во фреймворк .NET была добавлена библиотека параллельных задач TPL (Task Parallel Library), основной функционал которой располагается в пространстве имен **System.Threading.Tasks**. Данная библиотека упрощает работу с многопроцессорными, многоядерными системами. Кроме того, она упрощает работу по созданию новых потоков. Поэтому обычно рекомендуется использовать именно **TPL** и её классы для создания многопоточных приложений, хотя стандартные средства и класс **Thread** по-прежнему находят широкое применение.

В основе библиотеки **TPL** лежит концепция задач, каждая из которых описывает отдельную продолжительную операцию. В библиотеке классов .NET задача представлена специальным классом - классом **Task**, который находится в пространстве имен **System.Threading.Tasks**. Данный класс описывает отдельную задачу, которая запускается асинхронно в одном из потоков из пула потоков. Хотя её также можно запускать синхронно в текущем потоке.

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

* Первый способ создание объекта **Task** и вызов у него метода *Start*:

    ```cs
    Task task = new Task(() => Console.WriteLine("Hello Task!"));
    task.Start();
    ```

    В качестве параметра объект **Task** принимает делегат Action, то есть мы можем передать любое действие, которое соответствует данному делегату, например, лямбда-выражение, как в данном случае, или ссылку на какой-либо метод. То есть в данном случае при выполнении задачи на консоль будет выводиться строка "Hello Task!".

    А метод *Start()* собственно запускает задачу.

* Второй способ заключается в использовании статического метода **Task.Factory.StartNew()**. Этот метод также в качестве параметра принимает делегат Action, который указывает, какое действие будет выполняться. При этом этот метод сразу же запускает задачу:

    ```cs
    Task task = Task.Factory.StartNew(() => Console.WriteLine("Hello Task!"));
    ```

    В качестве результата метод возвращает запущенную задачу.

* Третий способ определения и запуска задач представляет использование статического метода Task.Run():

    ```cs
    Task task = Task.Run(() => Console.WriteLine("Hello Task!"));
    ```

    Метод **Task.Run()** также в качестве параметра может принимать делегат Action - выполняемое действие и возвращает объект **Task**.

Определим небольшую программу, где используем все эти способы:

```cs
Task task1 = new Task(() => Console.WriteLine("Task1 is executed"));
task1.Start();
 
Task task2 = Task.Factory.StartNew(() => Console.WriteLine("Task2 is executed"));
 
Task task3 = Task.Run(() => Console.WriteLine("Task3 is executed"));
```

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

### Ожидание завершения задачи

Чтобы приложение ожидало завершения задачи, можно использовать метод *Wait()* объекта **Task**:

```cs
Task task1 = new Task(() => Console.WriteLine("Task1 is executed"));
task1.Start();
 
Task task2 = Task.Factory.StartNew(() => Console.WriteLine("Task2 is executed"));
 
Task task3 = Task.Run(() => Console.WriteLine("Task3 is executed"));
 
task1.Wait();   // ожидаем завершения задачи task1
task2.Wait();   // ожидаем завершения задачи task2
task3.Wait();   // ожидаем завершения задачи task3
```

Возможный консольный вывод программы:

```
Task3 is executed
Task2 is executed
Task1 is executed
```

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

Стоит отметить, что метод *Wait()* блокирует вызывающий поток, в котором запущена задача, пока эта задача не завершит свое выполнение.

### Синхронный запуск задачи

По умолчанию задачи запускаются асинхронно. Однако с помощью метода *RunSynchronously()* можно запускать синхронно:

```cs
Console.WriteLine("Main Starts");
// создаем задачу
Task task1 = new Task(() =>
{
    Console.WriteLine("Task Starts");
    Thread.Sleep(1000); 
    Console.WriteLine("Task Ends");
 });
task1.RunSynchronously(); // запускаем задачу синхронно
Console.WriteLine("Main Ends"); // этот вызов ждет завершения задачи task1 
```

### Массив задач

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

```cs
Task[] tasks1 = new Task[3]
{
    new Task(() => Console.WriteLine("First Task")),
    new Task(() => Console.WriteLine("Second Task")),
    new Task(() => Console.WriteLine("Third Task"))
};
// запуск задач в массиве
foreach (var t in tasks1)
    t.Start();
```

Если необходимо завершить выполнение программы или вообще выполнять некоторый код лишь после того, как все задачи из массива завершатся, то применяется метод `Task.WaitAll(tasks)`:

```cs
Task[] tasks = new Task[3];
for(var i = 0; i < tasks.Length; i++)
{
    tasks[i] = new Task(() =>
    {
        Thread.Sleep(1000); // эмуляция долгой работы
        Console.WriteLine($"Task{i} finished");
    });
    tasks[i].Start();   // запускаем задачу
}
Console.WriteLine("Завершение метода Main");
 
Task.WaitAll(tasks); // ожидаем завершения всех задач
```

### Возвращение результатов из задач

Задачи могут не только выполняться как процедуры, но и возвращать определенные результаты:

```cs
int n1 =4, n2 = 5;
Task<int> sumTask = new Task<int>(() => Sum(n1, n2));
sumTask.Start();
 
int result = sumTask.Result;
Console.WriteLine($"{n1} + {n2} = {result}"); // 4 + 5 = 9
 
int Sum(int a, int b) => a + b;
```

Во-первых, чтобы получать из задачи некоторый результат, необходимо типизировать объект **Task** тем типом, объект которого мы хотим получить из задачи. Например, в примере выше мы ожидаем из задачи sumTask получить число типа **int**, соответственно типизируем объект **Task** данным типом - **Task<int>**.

И, во-вторых, в качестве задачи должен выполняться метод, который возвращает данный тип объекта. Так, в данном случае у нас в качестве задачи выполняется метод **Sum**, которая принимаетдва числа и на выходе возвращает их сумму - значение типа int.

Возвращаемое число будет храниться в свойстве **Result**: `sumTask.Result`. Нам не надо его приводить к типу int, оно уже само по себе будет представлять число.

```cs
int result = sumTask.Result;
```

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

## Концепция асинхронности

<!-- https://habr.com/ru/post/470830/ -->

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

Асинхронность — понятие весьма обширное и может достигаться многими путями.
В истории .NET можно выделить следующие:

1. EAP (Event-based Asynchronous Pattern) — как следует из названия, поход основан на событиях, которые срабатывают по завершении операции и обычного метода, вызывающего эту операцию
2. APM (Asynchronous Programming Model) — основан на 2 методах. Метод BeginSmth возвращает интерфейс IAsyncResult. Метод EndSmth принимает IAsyncResult (если к моменту вызова EndSmth операция не завершена, поток блокируется)
3. TAP (Task-based Asynchronous Pattern) — тот самый async/await (если говорить строго, то эти слова появились уже после появления подхода и типов Task и Task<TResult>, но async/await значительно улучшил эту концепцию)

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

### Task-based asynchronous pattern. Синтаксис и условия компиляции

Стандартный асинхронный метод в стиле **TAP** написать очень просто.

Для этого нужно:

1. Чтобы возвращаемое значение было Task, Task<T> или void (не рекомендуется).
2. Чтобы метод был помечен ключевым словом **async**, а внутри содержал **await**. Это ключевые слова идут в паре. При этом если метод содержит **await**, обязательно его помечать **async**, обратное неверно, но бессмысленно
3. Для приличия соблюдать конвенцию о суффиксе *Async*. Разумеется, компилятор за ошибку считать это не будет.

Было упомянуто, что метод должен содержать ключевое слово **await**. Оно (слово) указывает на необходимость асинхронного ожидания выполнения задачи, которую представляет тот объект задачи, к которому оно применяется.

### Работа с применением TAP

Сложно идти в дебри не понимая, как что-то должно работать. Рассмотрим **TAP** с точки зрения поведения программы.

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

Возьмем наипростейший пример, в качестве асинхронной операции возьмем **Task.Delay**, который осуществляет задержку на указанное время, не блокируя поток.

```cs
public static async Task DelayOperationAsync() // асинхронный метод
{
    BeforeCall();
    Task task = Task.Delay(1000); //асинхронная операция
    AfterCall();
    await task;
    AfterAwait();
}
``` 

Выполнение метода с точки зрения поведения происходит так.

1. Выполняется весь код, предшествующий вызову асинхронной операции. В данном случае это метод **BeforeCall**
2. Выполняется вызов асинхронной операции. На данном этапе поток не освобождается и не блокируется. Данная операция возвращает результат — упомянутый объект задачи (как правило Task), который сохраняется в локальную переменную
3. Выполняется код после вызова асинхронной операции, но до ожидания (await). В примере — **AfterCall**
4. Ожидание завершения на объекте задачи (который сохранили в локальную переменную) — **await task**.

    Если асинхронная операция к этому моменту завершена, то выполнение продолжается синхронно, в том же потоке.

    Если асинхронная операция не завершена, то сохраняется код, который надо будет вызвать по завершении асинхронной операции (т.н. продолжение), а поток возвращается в пул потоков и становится доступен для использования.

5. Выполнение операций после ожидания — **AfterAwait** — выполняется или сразу же, в том же потоке, когда операция на момент ожидания была завершена, или, по завершении операции, берется новый поток, который выполнит продолжение (сохраненное на предыдущем шаге)

<!-- https://metanit.com/sharp/tutorial/13.3.php -->

## Ещё раз про асинхронность другими словами

**Асинхронность** позволяет вынести отдельные задачи из основного потока в специальные асинхронные методы или блоки кода. Особенно это актуально в графических программах, где продолжительные задачи могу блокировать интерфейс пользователя. И чтобы этого не произошло, нужно задействовать асинхронность. Также асинхронность несёт выгоды в приложениях при обработке интернет запросов или при обращении к базам данных. При больших запросах к базе данных асинхронный метод просто уснёт на время, пока не получит данные от БД, а основной поток сможет продолжить свою работу. В синхронном же приложении, если бы код получения данных находился в основном потоке, этот поток просто бы блокировался на время получения данных.

Ключевыми для работы с асинхронными вызовами в C# являются два ключевых слова: **async** и **await**, цель которых - упростить написание асинхронного кода. Они используются вместе для создания асинхронного метода.

**Асинхонный метод** обладает следующими признаками:

* В заголовке метода используется модификатор async
* Метод содержит одно или несколько выражений await
* В качестве возвращаемого типа используется один из следующих:
    
    * void
    * Task
    * Task<T>
    * ValueTask<T>

Асинхронный метод, как и обычный, может использовать любое количество параметров или не использовать их вообще. Однако асинхронный метод не может определять параметры с модификаторами **out** и **ref**.

Также стоит отметить, что слово **async**, которое указывается в определении метода, не делает автоматически метод асинхронным. Оно лишь указывает, что данный метод может содержать одно или несколько выражений **await**.

Рассмотрим пример асинхронного метода:

```cs
using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace HelloApp
{
    class Program
    {
        static void Factorial()
        {
            int result = 1;
            for(int i = 1; i <= 6; i++)
            {
                result *= i;
            }
            // симулируем задержку
            Thread.Sleep(8000);
            Console.WriteLine($"Факториал равен {result}");
        }

        // определение асинхронного метода
        static async void FactorialAsync()
        {
            // выполняется синхронно
            Console.WriteLine("Начало метода FactorialAsync");

            // выполняется асинхронно
            await Task.Run(()=>Factorial());                

            Console.WriteLine("Конец метода FactorialAsync");
        }
 
        static void Main(string[] args)
        {
            // вызов асинхронного метода
            FactorialAsync();   
 
            Console.WriteLine("Введите число: ");
            int n = Int32.Parse(Console.ReadLine());
            Console.WriteLine($"Квадрат числа равен {n * n}");
             
            Console.Read();
        }
    }
}
```

Здесь прежде всего определен обычный метод подсчета факториала. Для имитации долгой работы в нем используется задержка на 8 секунд с помощью метода **Thread.Sleep()**. Условно это некоторый метод, который выполняет некоторую работу продолжительное время. Но для упрощения понимания он просто подсчитывает факториал числа 6.

Также здесь определен асинхронный метод *FactorialAsync()*. Асинхронным он является потому, что имеет в определении модификатор **async**, его возвращаемым типом является **void**, и в теле метода определено выражение **await**.

Выражение **await** определяет **задачу**, которая будет выполняться асинхронно. В данном случае подобная задача представляет выполнение функции факториала:

```cs
await Task.Run(()=>Factorial());
```

По негласным правилам в названии асинхроннных методов принято использовать суффикс *Async* - Factorial**Async**().

Сам факториал мы получаем в асинхронном методе **FactorialAsync**. Асинхронным он является потому, что он объявлен с модификатором **async** и содержит использование ключевого слова **await**.

И в методе Main мы вызываем этот асинхронный метод.

Посмотрим, какой у программы будет консольный вывод:

```
Начало метода FactorialAsync
Введите число: 
7
Квадрат числа равен 49
Конец метода Main
Факториал равен 720
Окончание метода FactorialAsync
```

Разберем поэтапно, что здесь происходит:

1. Запускается метод Main, в котором вызывается асинхронный метод *FactorialAsync*.

2. Метод *FactorialAsync* начинает выполняться **синхронно** вплоть до выражения **await**.

3. Выражение **await** запускает асинхронную задачу `Task.Run(()=>Factorial())`

4. Пока выполняется асинхронная задача `Task.Run(()=>Factorial())` (а она может выполняться довольно продожительное время), выполнение кода возвращается в вызывающий метод - то есть в метод Main. В методе Main нам будет предложено ввести число для вычисления квадрата числа.

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

5. Когда асинхронная задача завершила свое выполнение (в случае выше - подсчитала факториал числа), **продолжает** работу асинхронный метод *FactorialAsync*, который вызвал асинхронную задачу.

**Передача параметров в асинхронную операцию**

Выше вычислялся факториал 6, но, допустим, мы хотим вычислять факториалы разных чисел:

```cs
static async void FactorialAsync(int n)
{
    await Task.Run(()=>Factorial(n));
}
```

**Получение результата из асинхронной операции**

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

```cs
static async void FactorialAsync(int n)
{
    int x = await Task.Run(()=>Factorial(n));
    Console.WriteLine($"Факториал равен {x}");
}
```

Метод *Factorial* возвращает значение типа **int**, это значение мы можем получить, просто присвоив результат асинхронной операции переменной данного типа: `int x = await Task.Run(()=>Factorial(n));`

<!-- ## Ближе к телу -->