Перейти к содержанию

4. Внутреннее устройство Task

О главе

Цель: заглянуть внутрь объекта Task: какие у него поля, как в одном int закодированы состояние и опции, где хранятся продолжения и как задача их запускает при завершении.

Лабораторная: start/ — программа, которая печатает флаги и список продолжений задач, но методы доступа к приватным полям не написаны (TODO). final/ — методы доступа через [UnsafeAccessor] и три опыта: флаги (§4.2), продолжения (§4.3), кто выполняет продолжение (§4.4). Код — в конце главы.

Визуализация: Поле продолжений — Подписывайте продолжения на задачу и завершайте её: смотрите, когда в поле одиночное продолжение, когда List<object>, когда метка.

Статус: ✅ проверено на Windows 11, рантайм 10.0.12. Листинги — декомпиляция System.Private.CoreLib 10.0.12 (.\tools\disasm.ps1 -Assembly corelib -Type …).

Детали реализации

Всё в этой главе — внутренности рантайма, а не публичный контракт. Имена полей, значения битов и раскладка в памяти могут измениться в любой версии. Знать их полезно, чтобы понимать поведение, стоимость и вывод отладчика. Полагаться на них в продакшен-коде нельзя.

Под капотом: почему Task так называется

Имя Task («задача») осталось от другой эпохи. В .NET 4 (2010) Task появился в библиотеке параллельного программирования (TPL) как порция работы для пула потоков. Через два года C# 5 добавил async/await, и для «результата, который будет позже» повторно использовали уже готовый Task. Один класс получил две роли, а его статические методы смешали оба мира.

Читайте Task<int> как «int, который будет позже», а не как «работа, которую кто-то выполняет». Кто и как её делает (поток, таймер, сеть), тип не говорит. В других языках это разделено: Promise и Web Workers в JS, Future и thread::spawn в Rust.

Два вида задач, один тип (это видно в таблице полей §4.1 и в опыте §4.2):

Задача-работа Задача-обещание
Что это обёртка над делегатом, который надо выполнить «квитанция»: кто-то завершит её позже
Поле m_action делегат null или служебное значение
Занимает поток да, на время выполнения делегата нет
Откуда берётся Task.Run, Task.Factory.StartNew, new Task(…) + Start() async-методы, TaskCompletionSource, Task.Delay, Task.WhenAll/WhenAny

Отсюда разбор статических методов Task по ролям:

Метод Роль
Task.Run, Task.Factory.StartNew, а также Parallel.For/ForEach/Invoke параллелизм: отдают делегат пулу потоков
Task.Delay, Task.WhenAll, Task.WhenAny, Task.FromResult, Task.CompletedTask, Task.FromCanceled, Task.FromException асинхронность: работают с готовыми или будущими результатами, потоков не занимают

Правило пальца: если вы передаёте методу делегат с работой, это параллелизм. Если передаёте задачи или время, это комбинаторы асинхронного кода: Task.Delay(1000) ничего не выполняет и не держит поток, это просто будущее, которое наступит через секунду.

4.1. Поля Task

Task — обычный класс. Вот его поля экземпляра (декомпиляция CoreLib 10.0.12, порядок как в исходнике):

CoreLib 10.0.12: поля экземпляра Task
1
2
3
4
5
6
7
private int m_taskId;
internal Delegate m_action;
private protected object m_stateObject;
internal TaskScheduler m_taskScheduler;
internal volatile int m_stateFlags;
private volatile object m_continuationObject;
internal ContingentProperties m_contingentProperties;

А Task<TResult> добавляет одно поле:

CoreLib 10.0.12: поле Task<TResult>
internal TResult m_result;
Строка Поле Что хранит
1 m_taskId номер задачи для отладчика (Task.Id). Выдаётся лениво, при первом обращении
2 m_action делегат с работой: у Task.Run, new Task(…), ContinueWith. У задач-обещаний (TaskCompletionSource, async-методы, Task.Delay) — null или служебное значение
3 m_stateObject state из Task.Factory.StartNew(…, state) или AsyncState. Бокс async-метода хранит здесь захваченный ExecutionContext
4 m_taskScheduler планировщик, на котором задача запущена (для Task.Run — пул потоков)
5 m_stateFlags состояние и опции одним int (§4.2)
6 m_continuationObject продолжения: кого будить при завершении (§4.3)
7 m_contingentProperties «редкие» данные, которые нужны не всем задачам: исключения, токен отмены, событие для блокирующего Wait, родитель и дочерние задачи. Создаётся лениво, поэтому у большинства задач здесь null
— m_result результат Task<TResult>

Всё «редкое» вынесено в ContingentProperties, чтобы обычная задача оставалась маленькой. Насколько маленькой, мы видели в замерах: Task<int> занимает 72 байта на x64. В машинном коде из прошлых глав встречались смещения полей внутри объекта: m_stateFlags по смещению 0x34, m_result (у Task<int>) — 0x38, m_contingentProperties — 0x28.

Как добраться до приватных полей. В лабораторной используем атрибут [UnsafeAccessor] (.NET 8+). Объявляем extern-метод, а рантайм сам подставляет прямой доступ к полю: без отражения и почти без накладных расходов. Имя проверяется при первом вызове.

final/TaskInternals.cs
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_stateFlags")]
    public static extern ref int StateFlags(Task task);

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_continuationObject")]
    public static extern ref object? ContinuationObject(Task task);

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_action")]
    public static extern ref Delegate? Action(Task task);
  • Строки 7–8: ref int на поле m_stateFlags переданной задачи. volatile в объявлении поля для доступа значения не имеет.
  • Строки 10–11, 13–14: то же для m_continuationObject и m_action.

Задание

В start/TaskInternals.cs эти три метода — заглушки, которые бросают NotImplementedException, и программа печатает TODO вместо флагов. Замените их объявлениями с [UnsafeAccessor] по таблице полей выше. Запуск из папки chapters: dotnet run -c Release --project 04-task-internals\start.

4.2. m_stateFlags: состояние и опции в одном int

Публичное свойство Task.Status — не поле, а расшифровка m_stateFlags. Вот оно целиком:

CoreLib 10.0.12: Task.Status
public TaskStatus Status
{
    get
    {
        int stateFlags = m_stateFlags;
        if ((stateFlags & 0x200000) != 0)
        {
            return TaskStatus.Faulted;
        }
        if ((stateFlags & 0x400000) != 0)
        {
            return TaskStatus.Canceled;
        }
        if ((stateFlags & 0x1000000) != 0)
        {
            return TaskStatus.RanToCompletion;
        }
        if ((stateFlags & 0x800000) != 0)
        {
            return TaskStatus.WaitingForChildrenToComplete;
        }
        if ((stateFlags & 0x20000) != 0)
        {
            return TaskStatus.Running;
        }
        if ((stateFlags & 0x10000) != 0)
        {
            return TaskStatus.WaitingToRun;
        }
        if ((stateFlags & 0x2000000) != 0)
        {
            return TaskStatus.WaitingForActivation;
        }
        return TaskStatus.Created;
    }
}

Проверки идут по приоритету: сначала «упала» (строки 6–9), «отменена» (10–13), «успешно» (14–17), и только потом «выполняется» и «ждёт». Поэтому у задачи могут одновременно стоять, например, биты WaitingForActivation и Faulted, и Status честно покажет Faulted (это видно в опыте ниже).

Раскладка битов m_stateFlags (числа — из кода CoreLib; в скомпилированной сборке enum с именами не сохранился, имена — как в исходниках dotnet/runtime):

Биты Группа Значения
0x0000_00FF TaskCreationOptions — то, что вы передали при создании 0x08 DenyChildAttach, 0x40 RunContinuationsAsynchronously и др.
0x0000_FF00 InternalTaskOptions — служебные опции 0x400 PromiseTask, 0x800 HiddenState, 0x2000 QueuedByRuntime, 0x4000 DoNotDispose
0x0001_0000… состояние 0x10000 Started, 0x20000 DelegateInvoked, 0x200000 Faulted, 0x400000 Canceled, 0x1000000 RanToCompletion, 0x2000000 WaitingForActivation, 0x4000000 CompletionReserved

Полезные маски, которые встречаются в машинном коде:

Маска Смысл Где встречается
0x1600000 Faulted \| Canceled \| RanToCompletion — «задача завершена» IsCompleted, проверка в MoveNext после GetAwaiter()
0x11000000 против 0x1000000 «завершена успешно и отладчик не просил уведомления» быстрый путь GetResult()
0x5600000 «уже завершена или кто-то её завершает» TrySetResult (§4.4)

Опыт: флаги у задач разного происхождения

Первая часть final/Program.cs:

final/Program.cs
Console.WriteLine("== 1. Флаги состояния ==");
Show("new Task(() => {})", new Task(() => { }));
Show("Task.CompletedTask", Task.CompletedTask);
Show("Task.FromResult(1000)", Task.FromResult(1000));
var tcs = new TaskCompletionSource<int>();
Show("TCS.Task до SetResult", tcs.Task);
var tcsAsync = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
Show("TCS(RunContinuationsAsynchronously)", tcsAsync.Task);
Task delay = Task.Delay(Timeout.Infinite);
Show("Task.Delay(Infinite)", delay);
Task pendingAsync = WaitAsync(tcs.Task);                    // async-метод, приостановленный на tcs.Task
Show("async-метод в ожидании (бокс)", pendingAsync);
var gate = new ManualResetEventSlim();
Task running = Task.Run(() => gate.Wait());
Thread.Sleep(100);                                          // дать Task.Run начать выполняться
Show("Task.Run, выполняется", running);
gate.Set();
await running;
Show("Task.Run, завершён", running);
Show("Task.FromException", Task.FromException(new InvalidOperationException()));
Show("Task.FromCanceled", Task.FromCanceled(new CancellationToken(canceled: true)));
  new Task(() => {})                     Created              0x00000000 =
  Task.CompletedTask                     RanToCompletion      0x01004000 = DoNotDispose | RanToCompletion
  Task.FromResult(1000)                  RanToCompletion      0x01000000 = RanToCompletion
  TCS.Task до SetResult                  WaitingForActivation 0x02000400 = PromiseTask | WaitingForActivation
  TCS(RunContinuationsAsynchronously)    WaitingForActivation 0x02000440 = RunContinuationsAsynchronously | PromiseTask | WaitingForActivation
  Task.Delay(Infinite)                   WaitingForActivation 0x02000400 = PromiseTask | WaitingForActivation
  async-метод в ожидании (бокс)          WaitingForActivation 0x02000C00 = PromiseTask | HiddenState | WaitingForActivation
  Task.Run, выполняется                  Running              0x00032008 = DenyChildAttach | QueuedByRuntime | Started | DelegateInvoked
  Task.Run, завершён                     RanToCompletion      0x01032008 = DenyChildAttach | QueuedByRuntime | Started | DelegateInvoked | RanToCompletion
  Task.FromException                     Faulted              0x06200400 = PromiseTask | Faulted | WaitingForActivation | CompletionReserved
  Task.FromCanceled                      Canceled             0x00500000 = CancellationAcknowledged | Canceled
  • Строка 1 вывода (строка 8 кода): new Task(…) без Start() — ни одного бита, Created. Это единственный способ увидеть Created: задачи из Task.Run и async-методов в нём не бывают.
  • Строка 2 (строка 9): Task.CompletedTask — общая задача на весь процесс. Бит DoNotDispose не даёт её случайно уничтожить через Dispose().
  • Строки 4, 6 (строки 11–12, 15–16): TaskCompletionSource и Task.Delay — обещания: PromiseTask | WaitingForActivation. Кода внутри нет, их завершит внешнее событие.
  • Строка 5 (строки 13–14): опция RunContinuationsAsynchronously (0x40) лежит в младшем байте тех же флагов. Как она меняет поведение — §4.4.
  • Строка 7 (строки 17–18): задача async-метода — это бокс машины состояний: тоже обещание плюс HiddenState (0x800). Этот бит ставит конструктор бокса, m_stateObject у него занят служебными данными и не показывается как AsyncState.
  • Строки 8–9 (строки 19–25): Task.Run — работа: Started (поставлена в очередь), DelegateInvoked (делегат запущен), QueuedByRuntime. И DenyChildAttach: Task.Run запрещает вложенным задачам прикрепляться к себе как к родителю. Это одно из отличий от Task.Factory.StartNew (глава 10).
  • Строка 10 (строка 26): у FromException остался бит WaitingForActivation, но Status — Faulted, потому что проверка Faulted идёт первой (строки 6–9 листинга Status). CompletionReserved — след того, как задачу завершали (§4.4).

4.3. m_continuationObject: кого будить при завершении

Поле m_continuationObject типа object хранит одно из четырёх:

Значение Когда
null продолжений нет
сам объект-продолжение (бокс машины, делегат, TaskContinuation…) ровно одно продолжение — самый частый случай
List<object> два и больше продолжений
s_taskCompletionSentinel (просто new object()) задача завершена, продолжения уже запущены

Добавление продолжения из CoreLib 10.0.12:

CoreLib 10.0.12: Task.AddTaskContinuation и AddTaskContinuationComplex
private bool AddTaskContinuation(object tc, bool addBeforeOthers)
{
    if (IsCompleted)
    {
        return false;
    }
    if (m_continuationObject != null || Interlocked.CompareExchange(ref m_continuationObject, tc, null) != null)
    {
        return AddTaskContinuationComplex(tc, addBeforeOthers);
    }
    return true;
}

private bool AddTaskContinuationComplex(object tc, bool addBeforeOthers)
{
    object continuationObject = m_continuationObject;
    if (continuationObject == s_taskCompletionSentinel)
    {
        return false;
    }
    List<object> list = continuationObject as List<object>;
    if (list == null)
    {
        list = new List<object>();
        if (addBeforeOthers)
        {
            list.Add(tc);
            list.Add(continuationObject);
        }
        else
        {
            list.Add(continuationObject);
            list.Add(tc);
        }
        object obj = continuationObject;
        continuationObject = Interlocked.CompareExchange(ref m_continuationObject, list, obj);
        if (continuationObject == obj)
        {
            return true;
        }
        list = continuationObject as List<object>;
        if (list == null)
        {
            return false;
        }
    }
    lock (list)
    {
        if (m_continuationObject == s_taskCompletionSentinel)
        {
            return false;
        }
        if (list.Count == list.Capacity)
        {
            list.RemoveAll((object l) => l == null);
        }
        if (addBeforeOthers)
        {
            list.Insert(0, tc);
        }
        else
        {
            list.Add(tc);
        }
    }
    return true;
}
  • Строки 3–6: задача уже завершена — добавлять некуда. Вызывающий (await) тогда выполнит продолжение сам.
  • Строка 7: быстрый путь. Если поле пустое, атомарно положить продолжение прямо в него (CompareExchange), без аллокаций. Так бывает в подавляющем большинстве случаев: у задачи один await.
  • Строки 17–20: гонка с завершением — поле уже стало меткой завершения.
  • Строки 21–34: второе продолжение. Создаётся List<object> с двумя элементами — аллокация списка и его массива. Затем список атомарно ставится на место одиночного продолжения (строка 36).
  • Строки 47–64: третье и следующие продолжения добавляются в список под lock.

Опыт: как растёт список продолжений

Вторая часть final/Program.cs:

final/Program.cs
Console.WriteLine("== 2. Список продолжений m_continuationObject ==");
var source = new TaskCompletionSource<int>();
Console.WriteLine($"  подписчиков 0: {Continuations(source.Task)}");
Task w1 = WaitAsync(source.Task);
Console.WriteLine($"  подписчиков 1: {Continuations(source.Task)}");
Task w2 = WaitAsync(source.Task);
Console.WriteLine($"  подписчиков 2: {Continuations(source.Task)}");
Task w3 = source.Task.ContinueWith(_ => { });
Console.WriteLine($"  подписчиков 3: {Continuations(source.Task)}");
source.SetResult(1);
Console.WriteLine($"  после SetResult: {Continuations(source.Task)}");
await Task.WhenAll(w1, w2, w3);
1
2
3
4
5
  подписчиков 0: null (продолжений нет)
  подписчиков 1: бокс машины
  подписчиков 2: List<object>, элементов: 2 (бокс машины, бокс машины)
  подписчиков 3: List<object>, элементов: 3 (бокс машины, бокс машины, ContinueWithTaskContinuation)
  после SetResult: object — метка «задача завершена» (s_taskCompletionSentinel)
  • Строка 2 вывода (строка 33 кода): await в WaitAsync положил в поле сам бокс своей машины. Ни делегата, ни обёртки (почему так — глава 3: builder передаёт TaskAwaiter бокс напрямую).
  • Строка 3 (строка 35): второй await — поле превратилось в список из двух боксов.
  • Строка 4 (строка 37): ContinueWith добавляет объект ContinueWithTaskContinuation — обёртку вокруг новой задачи-продолжения. Это заметно дороже, чем бокс.
  • Строка 5 (строка 39): после SetResult в поле лежит метка завершения. Список отдан на выполнение и больше не нужен.

Факт: второй await на ту же задачу стоит аллокацию списка

Один подписчик хранится прямо в поле. Со второго появляется List<object>. На это мы наткнулись в главе 3: когда несколько вызовов подписывались на одну задачу, их замер аллокаций «плыл» именно из-за этого списка. Если одну задачу ждут многие (общий кеш, ленивая инициализация), это нормально. Но для горячего пути «одна задача — один await» — лучший случай.

Интерактивная схема

Подписывайте продолжения на задачу и завершайте её: смотрите, когда в поле одиночное продолжение, когда List<object>, когда метка. Открыть на весь экран.

4.4. Завершение задачи и запуск продолжений

TrySetResult

Как задача-обещание становится завершённой (CoreLib 10.0.12, Task<TResult> и вспомогательный метод Task):

CoreLib 10.0.12: Task<TResult>.TrySetResult и Task.AtomicStateUpdate
internal bool TrySetResult(TResult result)
{
    bool result2 = false;
    if (AtomicStateUpdate(67108864, 90177536))
    {
        m_result = result;
        Interlocked.Exchange(ref m_stateFlags, m_stateFlags | 0x1000000);
        ContingentProperties contingentProperties = m_contingentProperties;
        if (contingentProperties != null)
        {
            NotifyParentIfPotentiallyAttachedTask();
            contingentProperties.SetCompleted();
        }
        FinishContinuations();
        result2 = true;
    }
    return result2;
}

internal bool AtomicStateUpdate(int newBits, int illegalBits)
{
    int stateFlags = m_stateFlags;
    if ((stateFlags & illegalBits) == 0)
    {
        if (Interlocked.CompareExchange(ref m_stateFlags, stateFlags | newBits, stateFlags) != stateFlags)
        {
            return AtomicStateUpdateSlow(newBits, illegalBits);
        }
        return true;
    }
    return false;
}
  • Строка 4: 67108864 = 0x4000000 (CompletionReserved), 90177536 = 0x5600000 (завершена или уже резервируется). Атомарно «застолбить» право завершить задачу. Если два потока одновременно вызовут TrySetResult, выиграет ровно один, а второй получит false. Отсюда разница между TrySetResult (вернёт false) и SetResult (бросит исключение).
  • Строки 22–28: само «застолбить» — один CompareExchange по полю флагов.
  • Строка 6: записать результат, и только потом (строка 7) поставить RanToCompletion. Кто увидит этот бит, гарантированно увидит и результат.
  • Строки 8–13: если кто-то ждёт задачу блокирующим Wait(), разбудить его событие (оно живёт в ContingentProperties).
  • Строка 14: запустить продолжения.

Этот же код встроенным мы видели в ассемблере MoveNext: lock cmpxchg по [rbx+0x34] с маской 0x5600000, затем запись 42 в [rbx+0x38] и FinishContinuations.

TaskCompletionSource<T> — тонкая обёртка: внутри одно поле Task<TResult> _task. Конструктор создаёт задачу-обещание, а TrySetResult/TrySetException/TrySetCanceled передают вызов ей.

FinishContinuations → RunContinuations

CoreLib 10.0.12: Task.FinishContinuations и начало RunContinuations (сокращено)
internal void FinishContinuations()
{
    object obj = Interlocked.Exchange(ref m_continuationObject, s_taskCompletionSentinel);
    if (obj != null)
    {
        RunContinuations(obj);
    }
}

private void RunContinuations(object continuationObject)
{
    ...
    bool flag2 = (m_stateFlags & 0x40) == 0 && RuntimeHelpers.TryEnsureSufficientExecutionStack();
    if (!(continuationObject is IAsyncStateMachineBox box))
    {
        ...                    // делегат, ContinueWith, ITaskCompletionAction, список — по тем же правилам
    }
    else
    {
        AwaitTaskContinuation.RunOrScheduleAction(box, flag2);
        ...
    }
}
  • Строка 3: атомарно забрать продолжения и поставить метку завершения. С этого момента новые подписчики (строки 3–6 AddTaskContinuation) увидят, что задача завершена.
  • Строка 13 — главное решение: можно ли выполнить продолжения прямо сейчас, на этом потоке (flag2, «разрешено инлайнить»). Нельзя, если:
    • у задачи стоит RunContinuationsAsynchronously (0x40);
    • стек и так глубокий: TryEnsureSufficientExecutionStack() защищает от переполнения стека, когда продолжения цепочкой завершают другие задачи.
  • Строки 14–22: выбор по типу продолжения. Самый частый случай (строки 18–22) — одно продолжение, и это бокс async-метода.

Дальше решает AwaitTaskContinuation:

CoreLib 10.0.12: AwaitTaskContinuation (фрагменты)
internal static void RunOrScheduleAction(IAsyncStateMachineBox box, bool allowInlining)
{
    Task t_currentTask = Task.t_currentTask;
    if (!allowInlining || !IsValidLocationForInlining)
    {
        ...
        ThreadPool.UnsafeQueueUserWorkItemInternal(box, preferLocal: true);
        return;
    }
    try
    {
        if (t_currentTask != null)
        {
            Task.t_currentTask = null;
        }
        box.MoveNext();
    }
    ...
}

internal static bool IsValidLocationForInlining
{
    get
    {
        SynchronizationContext current = SynchronizationContext.Current;
        if (current != null && current.GetType() != typeof(SynchronizationContext))
        {
            return false;
        }
        TaskScheduler internalCurrent = TaskScheduler.InternalCurrent;
        if (internalCurrent != null)
        {
            return internalCurrent == TaskScheduler.Default;
        }
        return true;
    }
}
  • Строки 4–9: инлайн запрещён — бокс ставится в очередь пула как рабочий элемент.
  • Строка 16: инлайн разрешён — MoveNext вашего метода вызывается прямо здесь, внутри TrySetResult того, кто завершил задачу.
  • Строки 21–36: ещё одно условие инлайна, о котором легко забыть: смотрится контекст завершающего потока. Если SetResult вызвали на UI-потоке (есть SynchronizationContext) или внутри нестандартного TaskScheduler, продолжения не выполняются в нём «втихую», а уходят в пул.

Опыт: инлайн или очередь

Третья часть final/Program.cs:

final/Program.cs
static async Task Who(string title, TaskCompletionSource<int> tcs)
{
    int setter = 0, continuation = 0;
    Task waiter = Continue();
    await Task.Run(() =>
    {
        setter = Environment.CurrentManagedThreadId;
        tcs.SetResult(1);                                   // здесь выполнится продолжение — или встанет в очередь
    });
    await waiter;
    Console.WriteLine($"  {title,-38} SetResult на потоке {setter}, продолжение на потоке {continuation}" +
                      (setter == continuation ? " — инлайн, внутри SetResult" : " — через очередь пула"));

    async Task Continue()
    {
        await tcs.Task;
        continuation = Environment.CurrentManagedThreadId;
    }
}
  обычная TCS                            SetResult на потоке 6, продолжение на потоке 6 — инлайн, внутри SetResult
  TCS(RunContinuationsAsynchronously)    SetResult на потоке 4, продолжение на потоке 7 — через очередь пула
  • Строка 1 вывода: продолжение (строки 68–69 кода) выполнилось на том же потоке и внутри вызова tcs.SetResult(1) (строка 60). Пока оно не закончится, SetResult не вернёт управление. Если бы продолжение было долгим или брало ту же блокировку, что и вызывающий, это задержало бы или подвесило вызывающего.
  • Строка 2 вывода: с RunContinuationsAsynchronously бит 0x40 запретил инлайн (строка 13 RunContinuations), и бокс ушёл в очередь пула. SetResult вернулся сразу.

Отсюда правило для библиотек, которые сами завершают чужие задачи через TaskCompletionSource: создавайте его с RunContinuationsAsynchronously, если нет причины поступить иначе. Подробно, с дедлоками и ConfigureAwait, — в главе 5.

4.5. Кеш готовых задач

Полностью про аллокации — в главе 3. Коротко о том, какие задачи рантайм не создаёт заново:

Что Объект
Task.CompletedTask, async Task без паузы одна общая задача на процесс (DoNotDispose в опыте §4.2)
Task.FromResult / async Task<T> без паузы со значением null или «нулём» простого типа общая s_defaultResultTask для каждого T
Task<bool> с true/false две общие задачи
Task<int> со значением от −1 до 8 массив из 10 общих задач

Все прочие результаты синхронного пути — новый Task<T> (72 байта для int).

4.6. Итоги

  • Task — небольшой объект: id, делегат (у «работы»), состояние, планировщик, флаги, продолжения и лениво создаваемые «редкие» свойства. Task<T> добавляет результат.
  • m_stateFlags в одном int хранит опции создания, служебные опции и состояние. Status — расшифровка этих битов по приоритету. Маска 0x1600000 = «завершена».
  • Задачи-обещания (TaskCompletionSource, Task.Delay, async-методы) несут PromiseTask и живут в WaitingForActivation, пока их не завершат. Задачи-работы (Task.Run) проходят Started → DelegateInvoked (Running).
  • Одно продолжение хранится прямо в поле без аллокаций, со второго появляется List<object>. После завершения в поле — метка.
  • Завершение: атомарно застолбить (CompletionReserved), записать результат, поставить RanToCompletion, забрать продолжения и выполнить их инлайн, если нет RunContinuationsAsynchronously, стек не глубокий и у завершающего потока нет своего контекста. Иначе — в пул.

Код лабораторной

Запуск из папки главы: dotnet run -c Release --project start или --project final.

start/Program.cs
// Глава 4, итог. Заглядываем внутрь Task через [UnsafeAccessor]:
//   1) m_stateFlags у задач разного происхождения (§4.2);
//   2) m_continuationObject: одно продолжение, список, метка завершения (§4.3);
//   3) кто выполняет продолжение: инлайн или очередь пула (§4.4).
using static TaskInternals;

Console.WriteLine("== 1. Флаги состояния ==");
Show("new Task(() => {})", new Task(() => { }));
Show("Task.CompletedTask", Task.CompletedTask);
Show("Task.FromResult(1000)", Task.FromResult(1000));
var tcs = new TaskCompletionSource<int>();
Show("TCS.Task до SetResult", tcs.Task);
var tcsAsync = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
Show("TCS(RunContinuationsAsynchronously)", tcsAsync.Task);
Task delay = Task.Delay(Timeout.Infinite);
Show("Task.Delay(Infinite)", delay);
Task pendingAsync = WaitAsync(tcs.Task);                    // async-метод, приостановленный на tcs.Task
Show("async-метод в ожидании (бокс)", pendingAsync);
var gate = new ManualResetEventSlim();
Task running = Task.Run(() => gate.Wait());
Thread.Sleep(100);                                          // дать Task.Run начать выполняться
Show("Task.Run, выполняется", running);
gate.Set();
await running;
Show("Task.Run, завершён", running);
Show("Task.FromException", Task.FromException(new InvalidOperationException()));
Show("Task.FromCanceled", Task.FromCanceled(new CancellationToken(canceled: true)));

Console.WriteLine();
Console.WriteLine("== 2. Список продолжений m_continuationObject ==");
var source = new TaskCompletionSource<int>();
Console.WriteLine($"  подписчиков 0: {Continuations(source.Task)}");
Task w1 = WaitAsync(source.Task);
Console.WriteLine($"  подписчиков 1: {Continuations(source.Task)}");
Task w2 = WaitAsync(source.Task);
Console.WriteLine($"  подписчиков 2: {Continuations(source.Task)}");
Task w3 = source.Task.ContinueWith(_ => { });
Console.WriteLine($"  подписчиков 3: {Continuations(source.Task)}");
source.SetResult(1);
Console.WriteLine($"  после SetResult: {Continuations(source.Task)}");
await Task.WhenAll(w1, w2, w3);

Console.WriteLine();
Console.WriteLine("== 3. Кто выполняет продолжение ==");
await Who("обычная TCS", new TaskCompletionSource<int>());
await Who("TCS(RunContinuationsAsynchronously)", new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously));

static void Show(string title, Task task) =>
    Console.WriteLine($"  {title,-38} {task.Status,-20} {Flags(task)}");

static async Task WaitAsync(Task task) => await task;

static async Task Who(string title, TaskCompletionSource<int> tcs)
{
    int setter = 0, continuation = 0;
    Task waiter = Continue();
    await Task.Run(() =>
    {
        setter = Environment.CurrentManagedThreadId;
        tcs.SetResult(1);                                   // здесь выполнится продолжение — или встанет в очередь
    });
    await waiter;
    Console.WriteLine($"  {title,-38} SetResult на потоке {setter}, продолжение на потоке {continuation}" +
                      (setter == continuation ? " — инлайн, внутри SetResult" : " — через очередь пула"));

    async Task Continue()
    {
        await tcs.Task;
        continuation = Environment.CurrentManagedThreadId;
    }
}
start/TaskInternals.cs
using System.Runtime.CompilerServices;

// Глава 4, заготовка. Доступ к приватным полям Task через [UnsafeAccessor] (.NET 8+): без отражения, с проверкой имени поля при первом вызове.
// Имена полей — из декомпиляции System.Private.CoreLib 10.0.12 (глава 4, §4.1). Это детали реализации, не контракт.
public static class TaskInternals
{
    // TODO: замените тела трёх методов на объявления с атрибутом
    //   [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<имя поля>")] public static extern ref <тип> Имя(Task task);
    // Имена и типы полей — в README главы 4, §4.1.
    public static ref int StateFlags(Task task) =>
        throw new NotImplementedException("TODO: доступ к m_stateFlags");

    public static ref object? ContinuationObject(Task task) =>
        throw new NotImplementedException("TODO: доступ к m_continuationObject");

    public static ref Delegate? Action(Task task) =>
        throw new NotImplementedException("TODO: доступ к m_action");

    // Расшифровка m_stateFlags. Числа — из кода CoreLib (Task.Status, InternalTaskOptions), имена — как в исходниках dotnet/runtime.
    static readonly (int Bit, string Name)[] Bits =
    [
        (0x1, "PreferFairness"), (0x2, "LongRunning"), (0x4, "AttachedToParent"), (0x8, "DenyChildAttach"),
        (0x10, "HideScheduler"), (0x40, "RunContinuationsAsynchronously"),                    // TaskCreationOptions
        (0x200, "ContinuationTask"), (0x400, "PromiseTask"), (0x800, "HiddenState"),
        (0x1000, "LazyCancellation"), (0x2000, "QueuedByRuntime"), (0x4000, "DoNotDispose"),  // InternalTaskOptions
        (0x10000, "Started"), (0x20000, "DelegateInvoked"), (0x40000, "Disposed"),
        (0x80000, "ExceptionObservedByParent"), (0x100000, "CancellationAcknowledged"),
        (0x200000, "Faulted"), (0x400000, "Canceled"), (0x800000, "WaitingOnChildren"),
        (0x1000000, "RanToCompletion"), (0x2000000, "WaitingForActivation"),
        (0x4000000, "CompletionReserved"), (0x10000000, "WaitCompletionNotification"),       // состояние
    ];

    public static string Flags(Task task)
    {
        int flags;
        try { flags = StateFlags(task); }
        catch (NotImplementedException e) { return e.Message; }
        var names = Bits.Where(b => (flags & b.Bit) != 0).Select(b => b.Name);
        return $"0x{flags:X8} = {string.Join(" | ", names)}";
    }

    public static string Continuations(Task task)
    {
        try { return Describe(ContinuationObject(task)); }
        catch (NotImplementedException e) { return e.Message; }
    }

    static string Describe(object? continuation) => continuation switch
    {
        null => "null (продолжений нет)",
        List<object> list => $"List<object>, элементов: {list.Count} ({string.Join(", ", list.Select(Short))})",
        { } o when o.GetType() == typeof(object) => "object — метка «задача завершена» (s_taskCompletionSentinel)",
        { } o => Short(o),
    };

    public static string Short(object? o) => o switch
    {
        null => "null",
        _ when o.GetType().Name.StartsWith("AsyncStateMachineBox") => "бокс машины",
        Delegate d => $"делегат {d.Method.Name}",
        _ => o.GetType().Name,
    };
}
final/Program.cs
// Глава 4, итог. Заглядываем внутрь Task через [UnsafeAccessor]:
//   1) m_stateFlags у задач разного происхождения (§4.2);
//   2) m_continuationObject: одно продолжение, список, метка завершения (§4.3);
//   3) кто выполняет продолжение: инлайн или очередь пула (§4.4).
using static TaskInternals;

Console.WriteLine("== 1. Флаги состояния ==");
Show("new Task(() => {})", new Task(() => { }));
Show("Task.CompletedTask", Task.CompletedTask);
Show("Task.FromResult(1000)", Task.FromResult(1000));
var tcs = new TaskCompletionSource<int>();
Show("TCS.Task до SetResult", tcs.Task);
var tcsAsync = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
Show("TCS(RunContinuationsAsynchronously)", tcsAsync.Task);
Task delay = Task.Delay(Timeout.Infinite);
Show("Task.Delay(Infinite)", delay);
Task pendingAsync = WaitAsync(tcs.Task);                    // async-метод, приостановленный на tcs.Task
Show("async-метод в ожидании (бокс)", pendingAsync);
var gate = new ManualResetEventSlim();
Task running = Task.Run(() => gate.Wait());
Thread.Sleep(100);                                          // дать Task.Run начать выполняться
Show("Task.Run, выполняется", running);
gate.Set();
await running;
Show("Task.Run, завершён", running);
Show("Task.FromException", Task.FromException(new InvalidOperationException()));
Show("Task.FromCanceled", Task.FromCanceled(new CancellationToken(canceled: true)));

Console.WriteLine();
Console.WriteLine("== 2. Список продолжений m_continuationObject ==");
var source = new TaskCompletionSource<int>();
Console.WriteLine($"  подписчиков 0: {Continuations(source.Task)}");
Task w1 = WaitAsync(source.Task);
Console.WriteLine($"  подписчиков 1: {Continuations(source.Task)}");
Task w2 = WaitAsync(source.Task);
Console.WriteLine($"  подписчиков 2: {Continuations(source.Task)}");
Task w3 = source.Task.ContinueWith(_ => { });
Console.WriteLine($"  подписчиков 3: {Continuations(source.Task)}");
source.SetResult(1);
Console.WriteLine($"  после SetResult: {Continuations(source.Task)}");
await Task.WhenAll(w1, w2, w3);

Console.WriteLine();
Console.WriteLine("== 3. Кто выполняет продолжение ==");
await Who("обычная TCS", new TaskCompletionSource<int>());
await Who("TCS(RunContinuationsAsynchronously)", new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously));

static void Show(string title, Task task) =>
    Console.WriteLine($"  {title,-38} {task.Status,-20} {Flags(task)}");

static async Task WaitAsync(Task task) => await task;

static async Task Who(string title, TaskCompletionSource<int> tcs)
{
    int setter = 0, continuation = 0;
    Task waiter = Continue();
    await Task.Run(() =>
    {
        setter = Environment.CurrentManagedThreadId;
        tcs.SetResult(1);                                   // здесь выполнится продолжение — или встанет в очередь
    });
    await waiter;
    Console.WriteLine($"  {title,-38} SetResult на потоке {setter}, продолжение на потоке {continuation}" +
                      (setter == continuation ? " — инлайн, внутри SetResult" : " — через очередь пула"));

    async Task Continue()
    {
        await tcs.Task;
        continuation = Environment.CurrentManagedThreadId;
    }
}
final/TaskInternals.cs
using System.Runtime.CompilerServices;

// Доступ к приватным полям Task через [UnsafeAccessor] (.NET 8+): без отражения, с проверкой имени поля при первом вызове.
// Имена полей — из декомпиляции System.Private.CoreLib 10.0.12 (глава 4, §4.1). Это детали реализации, не контракт.
public static class TaskInternals
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_stateFlags")]
    public static extern ref int StateFlags(Task task);

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_continuationObject")]
    public static extern ref object? ContinuationObject(Task task);

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_action")]
    public static extern ref Delegate? Action(Task task);

    // Расшифровка m_stateFlags. Числа — из кода CoreLib (Task.Status, InternalTaskOptions), имена — как в исходниках dotnet/runtime.
    static readonly (int Bit, string Name)[] Bits =
    [
        (0x1, "PreferFairness"), (0x2, "LongRunning"), (0x4, "AttachedToParent"), (0x8, "DenyChildAttach"),
        (0x10, "HideScheduler"), (0x40, "RunContinuationsAsynchronously"),                    // TaskCreationOptions
        (0x200, "ContinuationTask"), (0x400, "PromiseTask"), (0x800, "HiddenState"),
        (0x1000, "LazyCancellation"), (0x2000, "QueuedByRuntime"), (0x4000, "DoNotDispose"),  // InternalTaskOptions
        (0x10000, "Started"), (0x20000, "DelegateInvoked"), (0x40000, "Disposed"),
        (0x80000, "ExceptionObservedByParent"), (0x100000, "CancellationAcknowledged"),
        (0x200000, "Faulted"), (0x400000, "Canceled"), (0x800000, "WaitingOnChildren"),
        (0x1000000, "RanToCompletion"), (0x2000000, "WaitingForActivation"),
        (0x4000000, "CompletionReserved"), (0x10000000, "WaitCompletionNotification"),       // состояние
    ];

    public static string Flags(Task task)
    {
        int flags = StateFlags(task);
        var names = Bits.Where(b => (flags & b.Bit) != 0).Select(b => b.Name);
        return $"0x{flags:X8} = {string.Join(" | ", names)}";
    }

    public static string Continuations(Task task) => ContinuationObject(task) switch
    {
        null => "null (продолжений нет)",
        List<object> list => $"List<object>, элементов: {list.Count} ({string.Join(", ", list.Select(Short))})",
        { } o when o.GetType() == typeof(object) => "object — метка «задача завершена» (s_taskCompletionSentinel)",
        { } o => Short(o),
    };

    public static string Short(object? o) => o switch
    {
        null => "null",
        _ when o.GetType().Name.StartsWith("AsyncStateMachineBox") => "бокс машины",
        Delegate d => $"делегат {d.Method.Name}",
        _ => o.GetType().Name,
    };
}