Цель: заглянуть внутрь объекта 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()
асинхронность: работают с готовыми или будущими результатами, потоков не занимают
Правило пальца: если вы передаёте методу делегат с работой, это параллелизм. Если передаёте задачи или время, это комбинаторы асинхронного кода: Task.Delay(1000) ничего не выполняет и не держит поток, это просто будущее, которое наступит через секунду.
номер задачи для отладчика (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-метод, а рантайм сам подставляет прямой доступ к полю: без отражения и почти без накладных расходов. Имя проверяется при первом вызове.
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_stateFlags")]publicstaticexternrefintStateFlags(Tasktask);[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_continuationObject")]publicstaticexternrefobject?ContinuationObject(Tasktask);[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_action")]publicstaticexternrefDelegate?Action(Tasktask);
Строки 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.
Проверки идут по приоритету: сначала «упала» (строки 6–9), «отменена» (10–13), «успешно» (14–17), и только потом «выполняется» и «ждёт». Поэтому у задачи могут одновременно стоять, например, биты WaitingForActivation и Faulted, и Status честно покажет Faulted (это видно в опыте ниже).
Раскладка битов m_stateFlags (числа — из кода CoreLib; в скомпилированной сборке enum с именами не сохранился, имена — как в исходниках dotnet/runtime):
Биты
Группа
Значения
0x0000_00FF
TaskCreationOptions — то, что вы передали при создании
0x08 DenyChildAttach, 0x40 RunContinuationsAsynchronously и др.
Console.WriteLine("== 1. Флаги состояния ==");Show("new Task(() => {})",newTask(()=>{}));Show("Task.CompletedTask",Task.CompletedTask);Show("Task.FromResult(1000)",Task.FromResult(1000));vartcs=newTaskCompletionSource<int>();Show("TCS.Task до SetResult",tcs.Task);vartcsAsync=newTaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);Show("TCS(RunContinuationsAsynchronously)",tcsAsync.Task);Taskdelay=Task.Delay(Timeout.Infinite);Show("Task.Delay(Infinite)",delay);TaskpendingAsync=WaitAsync(tcs.Task);// async-метод, приостановленный на tcs.TaskShow("async-метод в ожидании (бокс)",pendingAsync);vargate=newManualResetEventSlim();Taskrunning=Task.Run(()=>gate.Wait());Thread.Sleep(100);// дать Task.Run начать выполнятьсяShow("Task.Run, выполняется",running);gate.Set();awaitrunning;Show("Task.Run, завершён",running);Show("Task.FromException",Task.FromException(newInvalidOperationException()));Show("Task.FromCanceled",Task.FromCanceled(newCancellationToken(canceled:true)));
Строка 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
Строки 3–6: задача уже завершена — добавлять некуда. Вызывающий (await) тогда выполнит продолжение сам.
Строка 7: быстрый путь. Если поле пустое, атомарно положить продолжение прямо в него (CompareExchange), без аллокаций. Так бывает в подавляющем большинстве случаев: у задачи один await.
Строки 17–20: гонка с завершением — поле уже стало меткой завершения.
Строки 21–34: второе продолжение. Создаётся List<object> с двумя элементами — аллокация списка и его массива. Затем список атомарно ставится на место одиночного продолжения (строка 36).
Строки 47–64: третье и следующие продолжения добавляются в список под lock.
Строка 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: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 передают вызов ей.
internalvoidFinishContinuations(){objectobj=Interlocked.Exchange(refm_continuationObject,s_taskCompletionSentinel);if(obj!=null){RunContinuations(obj);}}privatevoidRunContinuations(objectcontinuationObject){...boolflag2=(m_stateFlags&0x40)==0&&RuntimeHelpers.TryEnsureSufficientExecutionStack();if(!(continuationObjectisIAsyncStateMachineBoxbox)){...// делегат, ContinueWith, ITaskCompletionAction, список — по тем же правилам}else{AwaitTaskContinuation.RunOrScheduleAction(box,flag2);...}}
Строка 3: атомарно забрать продолжения и поставить метку завершения. С этого момента новые подписчики (строки 3–6 AddTaskContinuation) увидят, что задача завершена.
Строка 13 — главное решение: можно ли выполнить продолжения прямо сейчас, на этом потоке (flag2, «разрешено инлайнить»). Нельзя, если:
у задачи стоит RunContinuationsAsynchronously (0x40);
стек и так глубокий: TryEnsureSufficientExecutionStack() защищает от переполнения стека, когда продолжения цепочкой завершают другие задачи.
Строки 14–22: выбор по типу продолжения. Самый частый случай (строки 18–22) — одно продолжение, и это бокс async-метода.
Строки 4–9: инлайн запрещён — бокс ставится в очередь пула как рабочий элемент.
Строка 16: инлайн разрешён — MoveNext вашего метода вызывается прямо здесь, внутри TrySetResult того, кто завершил задачу.
Строки 21–36: ещё одно условие инлайна, о котором легко забыть: смотрится контекст завершающего потока. Если SetResult вызвали на UI-потоке (есть SynchronizationContext) или внутри нестандартного TaskScheduler, продолжения не выполняются в нём «втихую», а уходят в пул.
staticasyncTaskWho(stringtitle,TaskCompletionSource<int>tcs){intsetter=0,continuation=0;Taskwaiter=Continue();awaitTask.Run(()=>{setter=Environment.CurrentManagedThreadId;tcs.SetResult(1);// здесь выполнится продолжение — или встанет в очередь});awaitwaiter;Console.WriteLine($" {title,-38} SetResult на потоке {setter}, продолжение на потоке {continuation}"+(setter==continuation?" — инлайн, внутри SetResult":" — через очередь пула"));asyncTaskContinue(){awaittcs.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.
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, стек не глубокий и у завершающего потока нет своего контекста. Иначе — в пул.
// Глава 4, итог. Заглядываем внутрь Task через [UnsafeAccessor]:// 1) m_stateFlags у задач разного происхождения (§4.2);// 2) m_continuationObject: одно продолжение, список, метка завершения (§4.3);// 3) кто выполняет продолжение: инлайн или очередь пула (§4.4).usingstaticTaskInternals;Console.WriteLine("== 1. Флаги состояния ==");Show("new Task(() => {})",newTask(()=>{}));Show("Task.CompletedTask",Task.CompletedTask);Show("Task.FromResult(1000)",Task.FromResult(1000));vartcs=newTaskCompletionSource<int>();Show("TCS.Task до SetResult",tcs.Task);vartcsAsync=newTaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);Show("TCS(RunContinuationsAsynchronously)",tcsAsync.Task);Taskdelay=Task.Delay(Timeout.Infinite);Show("Task.Delay(Infinite)",delay);TaskpendingAsync=WaitAsync(tcs.Task);// async-метод, приостановленный на tcs.TaskShow("async-метод в ожидании (бокс)",pendingAsync);vargate=newManualResetEventSlim();Taskrunning=Task.Run(()=>gate.Wait());Thread.Sleep(100);// дать Task.Run начать выполнятьсяShow("Task.Run, выполняется",running);gate.Set();awaitrunning;Show("Task.Run, завершён",running);Show("Task.FromException",Task.FromException(newInvalidOperationException()));Show("Task.FromCanceled",Task.FromCanceled(newCancellationToken(canceled:true)));Console.WriteLine();Console.WriteLine("== 2. Список продолжений m_continuationObject ==");varsource=newTaskCompletionSource<int>();Console.WriteLine($" подписчиков 0: {Continuations(source.Task)}");Taskw1=WaitAsync(source.Task);Console.WriteLine($" подписчиков 1: {Continuations(source.Task)}");Taskw2=WaitAsync(source.Task);Console.WriteLine($" подписчиков 2: {Continuations(source.Task)}");Taskw3=source.Task.ContinueWith(_=>{});Console.WriteLine($" подписчиков 3: {Continuations(source.Task)}");source.SetResult(1);Console.WriteLine($" после SetResult: {Continuations(source.Task)}");awaitTask.WhenAll(w1,w2,w3);Console.WriteLine();Console.WriteLine("== 3. Кто выполняет продолжение ==");awaitWho("обычная TCS",newTaskCompletionSource<int>());awaitWho("TCS(RunContinuationsAsynchronously)",newTaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously));staticvoidShow(stringtitle,Tasktask)=>Console.WriteLine($" {title,-38} {task.Status,-20} {Flags(task)}");staticasyncTaskWaitAsync(Tasktask)=>awaittask;staticasyncTaskWho(stringtitle,TaskCompletionSource<int>tcs){intsetter=0,continuation=0;Taskwaiter=Continue();awaitTask.Run(()=>{setter=Environment.CurrentManagedThreadId;tcs.SetResult(1);// здесь выполнится продолжение — или встанет в очередь});awaitwaiter;Console.WriteLine($" {title,-38} SetResult на потоке {setter}, продолжение на потоке {continuation}"+(setter==continuation?" — инлайн, внутри SetResult":" — через очередь пула"));asyncTaskContinue(){awaittcs.Task;continuation=Environment.CurrentManagedThreadId;}}
usingSystem.Runtime.CompilerServices;// Глава 4, заготовка. Доступ к приватным полям Task через [UnsafeAccessor] (.NET 8+): без отражения, с проверкой имени поля при первом вызове.// Имена полей — из декомпиляции System.Private.CoreLib 10.0.12 (глава 4, §4.1). Это детали реализации, не контракт.publicstaticclassTaskInternals{// TODO: замените тела трёх методов на объявления с атрибутом// [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<имя поля>")] public static extern ref <тип> Имя(Task task);// Имена и типы полей — в README главы 4, §4.1.publicstaticrefintStateFlags(Tasktask)=>thrownewNotImplementedException("TODO: доступ к m_stateFlags");publicstaticrefobject?ContinuationObject(Tasktask)=>thrownewNotImplementedException("TODO: доступ к m_continuationObject");publicstaticrefDelegate?Action(Tasktask)=>thrownewNotImplementedException("TODO: доступ к m_action");// Расшифровка m_stateFlags. Числа — из кода CoreLib (Task.Status, InternalTaskOptions), имена — как в исходниках dotnet/runtime.staticreadonly(intBit,stringName)[]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"), // состояние ];publicstaticstringFlags(Tasktask){intflags;try{flags=StateFlags(task);}catch(NotImplementedExceptione){returne.Message;}varnames=Bits.Where(b=>(flags&b.Bit)!=0).Select(b=>b.Name);return$"0x{flags:X8} = {string.Join("|", names)}";}publicstaticstringContinuations(Tasktask){try{returnDescribe(ContinuationObject(task));}catch(NotImplementedExceptione){returne.Message;}}staticstringDescribe(object?continuation)=>continuationswitch{null=>"null (продолжений нет)",List<object>list=>$"List<object>, элементов: {list.Count} ({string.Join(",", list.Select(Short))})",{}owheno.GetType()==typeof(object)=>"object — метка «задача завершена» (s_taskCompletionSentinel)",{}o=>Short(o),};publicstaticstringShort(object?o)=>oswitch{null=>"null",_wheno.GetType().Name.StartsWith("AsyncStateMachineBox")=>"бокс машины",Delegated=>$"делегат {d.Method.Name}",_=>o.GetType().Name,};}
// Глава 4, итог. Заглядываем внутрь Task через [UnsafeAccessor]:// 1) m_stateFlags у задач разного происхождения (§4.2);// 2) m_continuationObject: одно продолжение, список, метка завершения (§4.3);// 3) кто выполняет продолжение: инлайн или очередь пула (§4.4).usingstaticTaskInternals;Console.WriteLine("== 1. Флаги состояния ==");Show("new Task(() => {})",newTask(()=>{}));Show("Task.CompletedTask",Task.CompletedTask);Show("Task.FromResult(1000)",Task.FromResult(1000));vartcs=newTaskCompletionSource<int>();Show("TCS.Task до SetResult",tcs.Task);vartcsAsync=newTaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);Show("TCS(RunContinuationsAsynchronously)",tcsAsync.Task);Taskdelay=Task.Delay(Timeout.Infinite);Show("Task.Delay(Infinite)",delay);TaskpendingAsync=WaitAsync(tcs.Task);// async-метод, приостановленный на tcs.TaskShow("async-метод в ожидании (бокс)",pendingAsync);vargate=newManualResetEventSlim();Taskrunning=Task.Run(()=>gate.Wait());Thread.Sleep(100);// дать Task.Run начать выполнятьсяShow("Task.Run, выполняется",running);gate.Set();awaitrunning;Show("Task.Run, завершён",running);Show("Task.FromException",Task.FromException(newInvalidOperationException()));Show("Task.FromCanceled",Task.FromCanceled(newCancellationToken(canceled:true)));Console.WriteLine();Console.WriteLine("== 2. Список продолжений m_continuationObject ==");varsource=newTaskCompletionSource<int>();Console.WriteLine($" подписчиков 0: {Continuations(source.Task)}");Taskw1=WaitAsync(source.Task);Console.WriteLine($" подписчиков 1: {Continuations(source.Task)}");Taskw2=WaitAsync(source.Task);Console.WriteLine($" подписчиков 2: {Continuations(source.Task)}");Taskw3=source.Task.ContinueWith(_=>{});Console.WriteLine($" подписчиков 3: {Continuations(source.Task)}");source.SetResult(1);Console.WriteLine($" после SetResult: {Continuations(source.Task)}");awaitTask.WhenAll(w1,w2,w3);Console.WriteLine();Console.WriteLine("== 3. Кто выполняет продолжение ==");awaitWho("обычная TCS",newTaskCompletionSource<int>());awaitWho("TCS(RunContinuationsAsynchronously)",newTaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously));staticvoidShow(stringtitle,Tasktask)=>Console.WriteLine($" {title,-38} {task.Status,-20} {Flags(task)}");staticasyncTaskWaitAsync(Tasktask)=>awaittask;staticasyncTaskWho(stringtitle,TaskCompletionSource<int>tcs){intsetter=0,continuation=0;Taskwaiter=Continue();awaitTask.Run(()=>{setter=Environment.CurrentManagedThreadId;tcs.SetResult(1);// здесь выполнится продолжение — или встанет в очередь});awaitwaiter;Console.WriteLine($" {title,-38} SetResult на потоке {setter}, продолжение на потоке {continuation}"+(setter==continuation?" — инлайн, внутри SetResult":" — через очередь пула"));asyncTaskContinue(){awaittcs.Task;continuation=Environment.CurrentManagedThreadId;}}