Лабораторная:start/ — двенадцать задач с местами для предсказаний. final/ — ответы, исправления и задача 7 на самодельном UI-контексте. Запуск одной задачи: dotnet run -c Release --project start -- 8. Запуск: dotnet run -c Release --project start (или final).
Статус: ✅ все ответы проверены запуском на Ubuntu 26.04 (.NET 10.0.12), по три запуска; нестабильные места помечены. Перепроверено на Windows 11 (8 ядер), по три запуска: все ответы совпали, отличаются только миллисекунды. Задачи 8–12 добавлены по материалу глав 5–10.
// в приложении с однопоточным SynchronizationContext (UI)voidButton_Click(){varr=GetAsync().Result;Console.WriteLine(r);}asyncTask<int>GetAsync(){awaitTask.Delay(100);return1;}
Задача 8. В каком порядке напечатаются строки? А если создать TaskCompletionSource с TaskCreationOptions.RunContinuationsAsynchronously?
1.A, C1, B, C2, D. Вызов Foo() выполняется синхронно до первого незавершённого await, поэтому C1 печатается до B.
2.0, 1, 2, 3. Task.CompletedTask уже завершён, продолжение идёт синхронно, Foo целиком выполняется внутри вызова, поэтому 2 до 3.
3.3 3 3 (в трёх запусках; теоретически возможны и другие значения, не 0 1 2): лямбды захватывают переменную i, а не значение; к моменту выполнения цикл завершён. Для for переменная одна на цикл. Исправление: var copy = i; внутри цикла. В foreach с C# 5 переменная новая на итерацию.
4.после вызова, потом x. Исключение не вылетает при вызове, оно лежит в задаче и бросается при await.
5.a и b запущены сразу и идут параллельно: вывод 10, около 103 мс (Linux) или 110 мс (Windows). Вариант await Get() + await Get() последовательный: 10, около 200 мс (Windows — 206–218 мс: Task.Delay на Windows округляется вверх до шага таймера ≈ 15,6 мс, см. главу 10).
6.finally, затем inner. finally выполнится при выходе из метода по исключению (после продолжения), потом исключение уходит в Task и пробрасывается в await.
7. Дедлок (в лабораторной Wait с таймаутом 2 с вместо бесконечного .Result, поэтому видно «результата нет»): .Result блокирует UI-поток, продолжение GetAsync хочет вернуться в UI-поток через контекст. Console.WriteLine(r) не выполнится. Лечение: сделать обработчик async void и await GetAsync(), либо ConfigureAwait(false) в GetAsync (но лучше не блокировать).
8.до SetResult, продолжение, после SetResult: у потока нет контекста синхронизации, и продолжение выполняется прямо внутриSetResult (глава 5). С RunContinuationsAsynchronously продолжение уходит в пул и порядок после SetResult / продолжениене определён: в 12 запусках 8 раз первым было «продолжение», 4 раза — «после SetResult».
9.Value = 1. Запись в AsyncLocal внутри вызываемого async-метода не видна вызывающему: у метода своя копия ExecutionContext (глава 7).
10.поймали: первая; в задаче исключений: 2. await бросает только первое исключение, но в Task.Exception лежат оба (глава 8).
11.C, B, A. Обработчики отмены вызываются в обратном порядке регистрации (глава 9).
12. Почти 0 мс (1–3 мс на Linux, 2–4 мс на Windows), Task<Task>. StartNew с async-лямбдой вернул Task<Task>; await t дожидается только запуска лямбды. Исправление: await t.Unwrap() или Task.Run (300 мс на Linux, 304–314 мс на Windows; глава 10).
// Глава 18. «Что выведет код». Сначала впишите ответ в PREDICT, потом запускайте.// Запуск одной задачи: dotnet run -c Release -- 3 (без аргумента — все по очереди)usingSystem.Collections.Concurrent;usingSystem.Diagnostics;varquiz=newDictionary<string,Func<Task>>{["1"]=Task1,["2"]=Task2,["3"]=Task3,["4"]=Task4,["5"]=Task5,["6"]=Task6,["7"]=Task7,["8"]=Task8,["9"]=Task9,["10"]=Task10,["11"]=Task11,["12"]=Task12,};foreach(var(n,run)inquiz.Where(q=>args.Length==0||args.Contains(q.Key))){Console.WriteLine($"== Задача {n} ==");awaitrun();Console.WriteLine();}// PREDICT:staticasyncTaskTask1(){Console.WriteLine("A");vart=Foo();Console.WriteLine("B");awaitt;Console.WriteLine("D");staticasyncTaskFoo(){Console.WriteLine("C1");awaitTask.Delay(100);Console.WriteLine("C2");}}// PREDICT:staticasyncTaskTask2(){Console.WriteLine("0");vart=Foo();Console.WriteLine("3");awaitt;staticasyncTaskFoo(){Console.WriteLine("1");awaitTask.CompletedTask;Console.WriteLine("2");}}// PREDICT:staticasyncTaskTask3(){vartasks=newList<Task>();for(inti=0;i<3;i++)tasks.Add(Task.Run(()=>Console.WriteLine(i)));awaitTask.WhenAll(tasks);}// PREDICT:staticasyncTaskTask4(){vart=Boom();Console.WriteLine("после вызова");try{awaitt;}catch(Exceptione){Console.WriteLine(e.Message);}staticasyncTaskBoom(){thrownewException("x");}}// PREDICT: что выведет и сколько займёт? А вариант await Get() + await Get()?staticasyncTaskTask5(){varsw=Stopwatch.StartNew();vara=Get();varb=Get();Console.WriteLine(awaita+awaitb);Console.WriteLine($"{sw.ElapsedMilliseconds} мс");sw.Restart();Console.WriteLine(awaitGet()+awaitGet());Console.WriteLine($"{sw.ElapsedMilliseconds} мс");staticasyncTask<int>Get(){awaitTask.Delay(100);return5;}}// PREDICT:staticasyncTaskTask6(){try{awaitRun();}catch(Exceptione){Console.WriteLine(e.Message);}staticasyncTaskRun(){try{awaitTask.Delay(10);thrownewInvalidOperationException("inner");}finally{Console.WriteLine("finally");}}}// PREDICT: что выведет «обработчик кнопки» в однопоточном контексте и почему?staticTaskTask7(){varui=newSingleThreadSyncContext();ui.Post(_=>{vart=GetAsync();if(t.Wait(TimeSpan.FromSeconds(2)))// в настоящем UI здесь .Result без таймаутаConsole.WriteLine(t.Result);elseConsole.WriteLine("(прошло 2 секунды, результата нет)");ui.Complete();},null);ui.RunOnCurrentThread();returnTask.CompletedTask;staticasyncTask<int>GetAsync(){awaitTask.Delay(100);return1;}}// PREDICT: в каком порядке напечатаются строки? А если TaskCreationOptions.RunContinuationsAsynchronously?staticasyncTaskTask8(){vartcs=newTaskCompletionSource<int>();varwaiter=Waiter(tcs.Task);Console.WriteLine("до SetResult");tcs.SetResult(1);Console.WriteLine("после SetResult");awaitwaiter;staticasyncTaskWaiter(Task<int>t){awaitt;Console.WriteLine("продолжение");}}// PREDICT: что напечатает последняя строка?staticasyncTaskTask9(){varlocal=newAsyncLocal<int>{Value=1};awaitInc(local);Console.WriteLine($"Value = {local.Value}");staticasyncTaskInc(AsyncLocal<int>a){a.Value=2;awaitTask.Yield();}}// PREDICT: что поймает catch и сколько исключений лежит в задаче?staticasyncTaskTask10(){varall=Task.WhenAll(Fail("первая",20),Fail("вторая",50));try{awaitall;}catch(Exceptione){Console.WriteLine($"поймали: {e.Message}; в задаче исключений: {all.Exception!.InnerExceptions.Count}");}staticasyncTaskFail(stringmsg,intdelay){awaitTask.Delay(delay);thrownewInvalidOperationException(msg);}}// PREDICT: в каком порядке выполнятся обработчики отмены?staticTaskTask11(){usingvarcts=newCancellationTokenSource();cts.Token.Register(()=>Console.WriteLine("A"));cts.Token.Register(()=>Console.WriteLine("B"));cts.Token.Register(()=>Console.WriteLine("C"));cts.Cancel();returnTask.CompletedTask;}// PREDICT: сколько миллисекунд ждёт await? Что такое тип t?staticasyncTaskTask12(){varsw=Stopwatch.StartNew();vart=Task.Factory.StartNew(async()=>{awaitTask.Delay(300);});awaitt;Console.WriteLine($"await t закончился через {sw.ElapsedMilliseconds} мс; тип t: {t.GetType().Name}");}sealedclassSingleThreadSyncContext:SynchronizationContext{privatereadonlyBlockingCollection<(SendOrPostCallbackcb,object?state)>_queue=new();publicoverridevoidPost(SendOrPostCallbackd,object?state)=>_queue.Add((d,state));publicvoidComplete()=>_queue.CompleteAdding();publicvoidRunOnCurrentThread(){varprevious=Current;SetSynchronizationContext(this);foreach(var(cb,state)in_queue.GetConsumingEnumerable())cb(state);SetSynchronizationContext(previous);}}
// Глава 18, итог. Те же задачи с ответами; задача 3 дополнена исправлением, задача 7 — двумя исправлениями.// Запуск одной задачи: dotnet run -c Release -- 3 (без аргумента — все по очереди)usingSystem.Collections.Concurrent;usingSystem.Diagnostics;varquiz=newDictionary<string,Func<Task>>{["1"]=Task1,["2"]=Task2,["3"]=Task3,["4"]=Task4,["5"]=Task5,["6"]=Task6,["7"]=Task7,["8"]=Task8,["9"]=Task9,["10"]=Task10,["11"]=Task11,["12"]=Task12,};foreach(var(n,run)inquiz.Where(q=>args.Length==0||args.Contains(q.Key))){Console.WriteLine($"== Задача {n} ==");awaitrun();Console.WriteLine();}// ОТВЕТ: A, C1, B, C2, D. Foo выполняется синхронно до первого незавершённого await.staticasyncTaskTask1(){Console.WriteLine("A");vart=Foo();Console.WriteLine("B");awaitt;Console.WriteLine("D");staticasyncTaskFoo(){Console.WriteLine("C1");awaitTask.Delay(100);Console.WriteLine("C2");}}// ОТВЕТ: 0, 1, 2, 3. await на завершённой задаче не приостанавливает, Foo целиком проходит внутри вызова.staticasyncTaskTask2(){Console.WriteLine("0");vart=Foo();Console.WriteLine("3");awaitt;staticasyncTaskFoo(){Console.WriteLine("1");awaitTask.CompletedTask;Console.WriteLine("2");}}// ОТВЕТ: чаще всего 3 3 3: лямбды захватывают одну переменную i на весь цикл for.staticasyncTaskTask3(){vartasks=newList<Task>();for(inti=0;i<3;i++)tasks.Add(Task.Run(()=>Console.WriteLine(i)));awaitTask.WhenAll(tasks);Console.WriteLine("исправлено (копия на итерацию), порядок любой:");tasks.Clear();for(inti=0;i<3;i++){intcopy=i;tasks.Add(Task.Run(()=>Console.WriteLine(copy)));}awaitTask.WhenAll(tasks);}// ОТВЕТ: «после вызова», затем x. Исключение лежит в задаче до await.staticasyncTaskTask4(){vart=Boom();Console.WriteLine("после вызова");try{awaitt;}catch(Exceptione){Console.WriteLine(e.Message);}staticasyncTaskBoom(){thrownewException("x");}}// ОТВЕТ: 10 примерно за 100 мс (обе задачи запущены до первого await); второй вариант — 10 примерно за 200 мс.staticasyncTaskTask5(){varsw=Stopwatch.StartNew();vara=Get();varb=Get();Console.WriteLine(awaita+awaitb);Console.WriteLine($"{sw.ElapsedMilliseconds} мс");sw.Restart();Console.WriteLine(awaitGet()+awaitGet());Console.WriteLine($"{sw.ElapsedMilliseconds} мс");staticasyncTask<int>Get(){awaitTask.Delay(100);return5;}}// ОТВЕТ: finally, затем inner.staticasyncTaskTask6(){try{awaitRun();}catch(Exceptione){Console.WriteLine(e.Message);}staticasyncTaskRun(){try{awaitTask.Delay(10);thrownewInvalidOperationException("inner");}finally{Console.WriteLine("finally");}}}// ОТВЕТ: дедлок. Wait блокирует единственный поток контекста, а продолжение GetAsync ждёт именно его.staticTaskTask7(){varui=newSingleThreadSyncContext();ui.Post(async_=>{vart=GetAsync();if(t.Wait(TimeSpan.FromSeconds(2)))Console.WriteLine(t.Result);elseConsole.WriteLine("(прошло 2 секунды, результата нет) — дедлок");// Исправление 1: обработчик async, ждём через await.Console.WriteLine($"await: {await GetAsync()}");// Исправление 2 (если блокировать всё же приходится): ConfigureAwait(false) внутри.vart2=GetNoCtxAsync();Console.WriteLine($"ConfigureAwait(false) + Wait: завершилось={t2.Wait(TimeSpan.FromSeconds(2))}");ui.Complete();},null);ui.RunOnCurrentThread();returnTask.CompletedTask;staticasyncTask<int>GetAsync(){awaitTask.Delay(100);return1;}staticasyncTask<int>GetNoCtxAsync(){awaitTask.Delay(100).ConfigureAwait(false);return1;}}// ОТВЕТ: до SetResult, продолжение, после SetResult: без контекста продолжение выполняется прямо внутри SetResult (глава 5).// С RunContinuationsAsynchronously порядок «после SetResult» и «продолжение» не определён: продолжение уходит в пул// и соревнуется с печатью (в 12 запусках: 8 раз «продолжение» первым, 4 раза «после SetResult» первым).staticasyncTaskTask8(){vartcs=newTaskCompletionSource<int>();varwaiter=Waiter(tcs.Task);Console.WriteLine("до SetResult");tcs.SetResult(1);Console.WriteLine("после SetResult");awaitwaiter;Console.WriteLine("с RunContinuationsAsynchronously:");vartcs2=newTaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);varwaiter2=Waiter(tcs2.Task);Console.WriteLine("до SetResult");tcs2.SetResult(1);Console.WriteLine("после SetResult");awaitwaiter2;staticasyncTaskWaiter(Task<int>t){awaitt;Console.WriteLine("продолжение");}}// ОТВЕТ: Value = 1. Запись в AsyncLocal внутри вызываемого async-метода не видна вызывающему (глава 7).staticasyncTaskTask9(){varlocal=newAsyncLocal<int>{Value=1};awaitInc(local);Console.WriteLine($"Value = {local.Value}");staticasyncTaskInc(AsyncLocal<int>a){a.Value=2;awaitTask.Yield();}}// ОТВЕТ: await бросает только первое исключение («первая»), но в задаче лежат оба (глава 8).staticasyncTaskTask10(){varall=Task.WhenAll(Fail("первая",20),Fail("вторая",50));try{awaitall;}catch(Exceptione){Console.WriteLine($"поймали: {e.Message}; в задаче исключений: {all.Exception!.InnerExceptions.Count}");}staticasyncTaskFail(stringmsg,intdelay){awaitTask.Delay(delay);thrownewInvalidOperationException(msg);}}// ОТВЕТ: C, B, A. Обработчики отмены вызываются в обратном порядке регистрации (глава 9).staticTaskTask11(){usingvarcts=newCancellationTokenSource();cts.Token.Register(()=>Console.WriteLine("A"));cts.Token.Register(()=>Console.WriteLine("B"));cts.Token.Register(()=>Console.WriteLine("C"));cts.Cancel();returnTask.CompletedTask;}// ОТВЕТ: почти 0 мс. StartNew с async-лямбдой вернул Task<Task>; await ждёт только внешнюю задачу, то есть запуск лямбды (глава 10).// Исправление: await t.Unwrap() или Task.Run.staticasyncTaskTask12(){varsw=Stopwatch.StartNew();vart=Task.Factory.StartNew(async()=>{awaitTask.Delay(300);});awaitt;Console.WriteLine($"await t закончился через {sw.ElapsedMilliseconds} мс; тип t: {t.GetType().Name}");sw.Restart();awaitTask.Run(async()=>{awaitTask.Delay(300);});Console.WriteLine($"Task.Run: {sw.ElapsedMilliseconds} мс");}sealedclassSingleThreadSyncContext:SynchronizationContext{privatereadonlyBlockingCollection<(SendOrPostCallbackcb,object?state)>_queue=new();publicoverridevoidPost(SendOrPostCallbackd,object?state)=>_queue.Add((d,state));publicvoidComplete()=>_queue.CompleteAdding();publicvoidRunOnCurrentThread(){varprevious=Current;SetSynchronizationContext(this);foreach(var(cb,state)in_queue.GetConsumingEnumerable())cb(state);SetSynchronizationContext(previous);}}