Возвращается ли поток в пул потоков, если async-метод выполняется синхронно?

Ответ

Нет, не возвращается. Если async-метод завершается синхронно (без реальной асинхронной работы), поток, который его вызвал, блокируется до полного завершения метода, как в обычном синхронном вызове.

Механизм работы: Ключевое слово async лишь позволяет компилятору переписать метод в состояниевую машину. Если эта машина не встречает незавершённую задачу (await), она выполняется целиком в текущем контексте синхронизации.

Примеры:

// Пример 1: Метод без await.
public async Task<int> CalculateSync()
{
    // Вся работа выполняется синхронно. Поток НЕ освобождается.
    int result = PerformHeavyComputation(); // Долгий расчёт
    return result; // Возвращается уже завершённая задача (Task.FromResult под капотом).
}

// Пример 2: Await на уже завершённой задаче.
public async Task<string> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out string value))
    {
        // Task.FromResult создаёт завершённую задачу. Await на ней мгновенен и синхронен.
        // Поток НЕ освобождается на время "ожидания".
        return value;
    }
    // Только здесь, при реальном асинхронном вызове, поток может быть освобождён.
    return await _database.GetValueAsync(key);
}

Когда поток ВОЗВРАЩАЕТСЯ в пул: Поток освобождается только в момент, когда await встречает задачу, которая ещё не завершена (например, await httpClient.GetAsync(...), await file.ReadAllTextAsync(...)). В этот момент состояние метода сохраняется, и поток возвращается в пул. Когда асинхронная операция завершится, продолжение метода может быть выполнено на любом свободном потоке из пула (или на исходном, если был контекст синхронизации, как в UI).

Вывод для разработчика: Не стоит помечать метод как async, если в нём нет реальных асинхронных операций. Это создаёт ненужные накладные расходы на создание объекта-машины состояний (AsyncStateMachine). Для простых синхронных методов, возвращающих Task, лучше использовать Task.FromResult.

Ответ 18+ 🔞

Да ты посмотри, какая история интересная вырисовывается! Вот сидишь ты, пишешь свой async-метод, думаешь: "Щас я тут await вставлю — и поток сразу в пул вернётся, красота!" А нихуя подобного, оказывается.

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

Как это под капотом устроено: Ключевое слово async — это просто команда компилятору: "Слушай, перепиши мне этот метод в ту самую хитрую состояниевую машину". А эта машина, если не видит перед await задачи, которая ещё в процессе, просто прогоняет весь код до конца в текущем потоке. И всё.

Вот, смотри на примерах, чтобы совсем понятно стало:

// Пример 1: Чисто синхронный метод под маской async.
public async Task<int> CalculateSync()
{
    // Тут нет ни одного настоящего await!
    // Поток зайдёт сюда и упрётся лбом в этот долгий расчёт.
    int result = PerformHeavyComputation(); // Сидит, ждёт, пыхтит.
    return result; // И вернёт уже готовую, завершённую таску.
}

// Пример 2: Await на чём-то, что уже давно готово.
public async Task<string> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out string value))
    {
        // Task.FromResult создаёт таску, которая УЖЕ выполнена.
        // Await на такой задаче — это просто формальность, мгновенная.
        // Поток никуда не уходит, сидит на месте.
        return value;
    }
    // А вот тут — да. Тут реальный асинхронный вызов, который может зависнуть.
    // Вот на этом месте поток и скажет "окей, я свободен" и вернётся в пул.
    return await _database.GetValueAsync(key);
}

Так когда же поток, наконец, возвращается в пул? Только в тот самый момент, когда await встречает задачу, которая ещё в работе, которая не завершена. Типа сетевого запроса, чтения с диска или любой другой операции, где есть ожидание. Вот тут состояние метода замораживается, поток освобождается и идёт делать другие дела. А когда операция дойдёт до ума, продолжение метода подхватит какой-нибудь свободный поток из пула (если, конечно, у тебя нет какого-нибудь контекста синхронизации, как в UI-приложении, где надо вернуться в тот же поток).

Вывод, который мозг выносит: Не лепи async на метод просто так, для красоты. Если внутри нет реальной асинхронщины — это пустая трата ресурсов на создание этой самой состояниевой машины. Для синхронных методов, которые должны вернуть Task, есть Task.FromResult. Используй его и не выёбывайся.