Как устроены асинхронные методы в C#?

Как именно выполняются задачи и где это происходит? <br/> Например, верно ли, что происходит выделение времени в 1 мс для переключения между выполнением задач? Если это так, чем это отличается от работы потоков? <br/> Либо, возможно, в процессе вычисляется количество инструкций в методе, и через каждые N инструкций осуществляется точка переключения. <br/> Существует ли механизм, при котором синхронный и асинхронный код выполняются одновременно? Является ли это новым приемом, или он уже известен? В теории, конвейер в таком случае не останавливается. <br/> <br/> Также упоминается наличие машины состояний, но не уточняется, как происходит переключение. Например, что будет с кодом, содержащим бесконечный цикл? <br/> <br/> Как компилируется и исполняется подобный код? <br/> <pre><code class="cs">int[]arr=new int[100_000];
            int k = 0;
            var t1=Task.Run(() =&gt;
            {
                while (true)
                {
                    for (int i = 0; i &lt; arr.Length; i++)
                    {
                        arr[i] = k;
                    }
                    k = Interlocked.Add(ref k, 1);
                }
            });


            var t2=Task.Run(() =&gt;
            {
                while (true)
                {
                    int c = 0;
                    for (int i = 0; i &lt; arr.Length; i++)
                    {
                        if (arr[i] != k)
                        {
                            c++;
                        }
                    }
                    Console.WriteLine(c);
                }
            });
            await Task.WhenAny(t1, t2);</code></pre>
  • 16 апреля 2025 г. 19:35
Ответы на вопрос 2
Асинхронные методы в C# основываются на концепции, называемой "машиной состояний" и "программистской модели", которая позволяет писать неблокирующий код с использованием ключевых слов `async` и `await`. Давайте рассмотрим это подробнее.

### Как работают асинхронные методы

1. **Асинхронность и ожидание**: 
   Асинхронные методы (обозначаемые как `async`) могут выполнять операции, которые потенциально могут занять время, не блокируя основной поток выполнения. Когда вы используете `await` для вызова асинхронного метода, управление передается обратно вызывающему потоку, пока ожидается результат.

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

### Работает ли это одновременно?

Асинхронное программирование в C# основано на модели невидимого потока. Это означает, что вы не создаете дополнительные потоки для выполнения асинхронных операций (как это делается с помощью класса `Thread` или `Task.Run`). Асинхронные методы работают с одним потоком, переключаясь между выполнением операций.

### Полное переключение между задачами

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

### Одновременное выполнение синхронного и асинхронного кода

Синхронный и асинхронный код могут выполняться одновременно, однако это зависит от того, как они организованы. Например, если в вашей программе есть блокирующий код (как бесконечный цикл) в одном потоке, он может заблокировать выполнение других операций, пока не завершится. В вашем примере с бесконечными циклами оба метода (`t1` и `t2`) работают в разных потоках, и их выполнение будет параллельным (в зависимости от планировщика потоков).

### Что происходит с бесконечным циклом?

Если у вас есть бесконечный цикл внутри асинхронного метода, это может блокировать выполнение других операций, если он выполняется в потоке, который необходим для выполнения асинхронных операций. Однако если вы запускаете его в `Task.Run`, как в вашем примере, это не повлияет на другие асинхронные операции, так как они могут выполняться в других потоках.

### Компиляция и выполнение кода

Когда вы компилируете асинхронный метод, компилятор создает класс-автомат (также известный как "state machine"), который управляет состоянием выполнения метода. Это позволяет отслеживать, когда метод должен ждать, и когда нужно продолжать его выполнение. 

В вашем примере:
```csharp
int[] arr = new int[100_000];
int k = 0;
var t1 = Task.Run(() => { /* бесконечный цикл */ });
var t2 = Task.Run(() => { /* бесконечный цикл */ });
await Task.WhenAny(t1, t2);
```
Оба метода выполняются в фоновом режиме, так как они запущены в разных задачах. Как только одна из задач завершится (что в данном случае может быть сложным из-за бесконечных циклов), `Task.WhenAny` вернет завершившуюся задачу.

Таким образом, асинхронные и многопоточные подходы имеют свои собственные механизмы обработки параллелизма и не блокируют основной поток, но, как вы заметили, в некоторой степени забирают ресурсы из окружающей среды, особенно если используются бесконечные циклы.
Вот вам перевод (часть первая, а всего там шесть частей) фундаменнтальной статьи по async/await одного из разрабочиков .NET в Microsoft (а именно - Stephen Toub). 
Читайте и просвещайтесь.
Похожие вопросы