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

18. Задачи «что выведет код»

О главе

Цель: Проверить себя: предсказать вывод кода.

Лабораторная: 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.

Сначала ответьте сами, потом открывайте.

Задача 1.

Console.WriteLine("A");
var t = Foo();
Console.WriteLine("B");
await t;
Console.WriteLine("D");

static async Task Foo()
{
    Console.WriteLine("C1");
    await Task.Delay(100);
    Console.WriteLine("C2");
}

Задача 2.

static async Task Foo()
{
    Console.WriteLine("1");
    await Task.CompletedTask;
    Console.WriteLine("2");
}
Console.WriteLine("0");
var t = Foo();
Console.WriteLine("3");
await t;

Задача 3.

var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
    tasks.Add(Task.Run(() => Console.WriteLine(i)));
await Task.WhenAll(tasks);

Задача 4.

var t = Boom();
Console.WriteLine("после вызова");
try { await t; } catch (Exception e) { Console.WriteLine(e.Message); }

static async Task Boom() { throw new Exception("x"); }

Задача 5.

static async Task<int> Get()
{
    await Task.Delay(100);
    return 5;
}
var a = Get();
var b = Get();
Console.WriteLine(await a + await b);
Сколько займёт выполнение? А если написать await Get() + await Get()?

Задача 6.

async Task Run()
{
    try { await Task.Delay(10); throw new InvalidOperationException("inner"); }
    finally { Console.WriteLine("finally"); }
}
try { await Run(); } catch (Exception e) { Console.WriteLine(e.Message); }

Задача 7. Что выведет и почему может зависнуть?

// в приложении с однопоточным SynchronizationContext (UI)
void Button_Click()
{
    var r = GetAsync().Result;
    Console.WriteLine(r);
}
async Task<int> GetAsync() { await Task.Delay(100); return 1; }

Задача 8. В каком порядке напечатаются строки? А если создать TaskCompletionSource с TaskCreationOptions.RunContinuationsAsynchronously?

var tcs = new TaskCompletionSource<int>();
var waiter = Waiter(tcs.Task);
Console.WriteLine("до SetResult");
tcs.SetResult(1);
Console.WriteLine("после SetResult");
await waiter;

static async Task Waiter(Task<int> t) { await t; Console.WriteLine("продолжение"); }

Задача 9. Что напечатает последняя строка?

var local = new AsyncLocal<int> { Value = 1 };
await Inc(local);
Console.WriteLine($"Value = {local.Value}");

static async Task Inc(AsyncLocal<int> a) { a.Value = 2; await Task.Yield(); }

Задача 10. Что поймает catch и сколько исключений лежит в задаче?

var all = Task.WhenAll(Fail("первая", 20), Fail("вторая", 50));
try { await all; }
catch (Exception e) { Console.WriteLine($"{e.Message}; {all.Exception!.InnerExceptions.Count}"); }

static async Task Fail(string msg, int delay) { await Task.Delay(delay); throw new InvalidOperationException(msg); }

Задача 11. В каком порядке выполнятся обработчики отмены?

using var cts = new CancellationTokenSource();
cts.Token.Register(() => Console.WriteLine("A"));
cts.Token.Register(() => Console.WriteLine("B"));
cts.Token.Register(() => Console.WriteLine("C"));
cts.Cancel();

Задача 12. Сколько миллисекунд ждёт await t? Какой тип у t?

var t = Task.Factory.StartNew(async () => { await Task.Delay(300); });
await t;

Ответы

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).

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

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

start/Program.cs
// Глава 18. «Что выведет код». Сначала впишите ответ в PREDICT, потом запускайте.
// Запуск одной задачи: dotnet run -c Release -- 3   (без аргумента — все по очереди)
using System.Collections.Concurrent;
using System.Diagnostics;

var quiz = new Dictionary<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) in quiz.Where(q => args.Length == 0 || args.Contains(q.Key)))
{
    Console.WriteLine($"== Задача {n} ==");
    await run();
    Console.WriteLine();
}

// PREDICT:
static async Task Task1()
{
    Console.WriteLine("A");
    var t = Foo();
    Console.WriteLine("B");
    await t;
    Console.WriteLine("D");

    static async Task Foo()
    {
        Console.WriteLine("C1");
        await Task.Delay(100);
        Console.WriteLine("C2");
    }
}

// PREDICT:
static async Task Task2()
{
    Console.WriteLine("0");
    var t = Foo();
    Console.WriteLine("3");
    await t;

    static async Task Foo()
    {
        Console.WriteLine("1");
        await Task.CompletedTask;
        Console.WriteLine("2");
    }
}

// PREDICT:
static async Task Task3()
{
    var tasks = new List<Task>();
    for (int i = 0; i < 3; i++)
        tasks.Add(Task.Run(() => Console.WriteLine(i)));
    await Task.WhenAll(tasks);
}

// PREDICT:
static async Task Task4()
{
    var t = Boom();
    Console.WriteLine("после вызова");
    try { await t; } catch (Exception e) { Console.WriteLine(e.Message); }

    static async Task Boom() { throw new Exception("x"); }
}

// PREDICT: что выведет и сколько займёт? А вариант await Get() + await Get()?
static async Task Task5()
{
    var sw = Stopwatch.StartNew();
    var a = Get();
    var b = Get();
    Console.WriteLine(await a + await b);
    Console.WriteLine($"{sw.ElapsedMilliseconds} мс");

    sw.Restart();
    Console.WriteLine(await Get() + await Get());
    Console.WriteLine($"{sw.ElapsedMilliseconds} мс");

    static async Task<int> Get()
    {
        await Task.Delay(100);
        return 5;
    }
}

// PREDICT:
static async Task Task6()
{
    try { await Run(); } catch (Exception e) { Console.WriteLine(e.Message); }

    static async Task Run()
    {
        try { await Task.Delay(10); throw new InvalidOperationException("inner"); }
        finally { Console.WriteLine("finally"); }
    }
}

// PREDICT: что выведет «обработчик кнопки» в однопоточном контексте и почему?
static Task Task7()
{
    var ui = new SingleThreadSyncContext();
    ui.Post(_ =>
    {
        var t = GetAsync();
        if (t.Wait(TimeSpan.FromSeconds(2)))      // в настоящем UI здесь .Result без таймаута
            Console.WriteLine(t.Result);
        else
            Console.WriteLine("(прошло 2 секунды, результата нет)");
        ui.Complete();
    }, null);
    ui.RunOnCurrentThread();
    return Task.CompletedTask;

    static async Task<int> GetAsync() { await Task.Delay(100); return 1; }
}

// PREDICT: в каком порядке напечатаются строки? А если TaskCreationOptions.RunContinuationsAsynchronously?
static async Task Task8()
{
    var tcs = new TaskCompletionSource<int>();
    var waiter = Waiter(tcs.Task);
    Console.WriteLine("до SetResult");
    tcs.SetResult(1);
    Console.WriteLine("после SetResult");
    await waiter;

    static async Task Waiter(Task<int> t) { await t; Console.WriteLine("продолжение"); }
}

// PREDICT: что напечатает последняя строка?
static async Task Task9()
{
    var local = new AsyncLocal<int> { Value = 1 };
    await Inc(local);
    Console.WriteLine($"Value = {local.Value}");

    static async Task Inc(AsyncLocal<int> a) { a.Value = 2; await Task.Yield(); }
}

// PREDICT: что поймает catch и сколько исключений лежит в задаче?
static async Task Task10()
{
    var all = Task.WhenAll(Fail("первая", 20), Fail("вторая", 50));
    try { await all; }
    catch (Exception e) { Console.WriteLine($"поймали: {e.Message}; в задаче исключений: {all.Exception!.InnerExceptions.Count}"); }

    static async Task Fail(string msg, int delay) { await Task.Delay(delay); throw new InvalidOperationException(msg); }
}

// PREDICT: в каком порядке выполнятся обработчики отмены?
static Task Task11()
{
    using var cts = new CancellationTokenSource();
    cts.Token.Register(() => Console.WriteLine("A"));
    cts.Token.Register(() => Console.WriteLine("B"));
    cts.Token.Register(() => Console.WriteLine("C"));
    cts.Cancel();
    return Task.CompletedTask;
}

// PREDICT: сколько миллисекунд ждёт await? Что такое тип t?
static async Task Task12()
{
    var sw = Stopwatch.StartNew();
    var t = Task.Factory.StartNew(async () => { await Task.Delay(300); });
    await t;
    Console.WriteLine($"await t закончился через {sw.ElapsedMilliseconds} мс; тип t: {t.GetType().Name}");
}

sealed class SingleThreadSyncContext : SynchronizationContext
{
    private readonly BlockingCollection<(SendOrPostCallback cb, object? state)> _queue = new();
    public override void Post(SendOrPostCallback d, object? state) => _queue.Add((d, state));
    public void Complete() => _queue.CompleteAdding();

    public void RunOnCurrentThread()
    {
        var previous = Current;
        SetSynchronizationContext(this);
        foreach (var (cb, state) in _queue.GetConsumingEnumerable())
            cb(state);
        SetSynchronizationContext(previous);
    }
}
final/Program.cs
// Глава 18, итог. Те же задачи с ответами; задача 3 дополнена исправлением, задача 7 — двумя исправлениями.
// Запуск одной задачи: dotnet run -c Release -- 3   (без аргумента — все по очереди)
using System.Collections.Concurrent;
using System.Diagnostics;

var quiz = new Dictionary<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) in quiz.Where(q => args.Length == 0 || args.Contains(q.Key)))
{
    Console.WriteLine($"== Задача {n} ==");
    await run();
    Console.WriteLine();
}

// ОТВЕТ: A, C1, B, C2, D. Foo выполняется синхронно до первого незавершённого await.
static async Task Task1()
{
    Console.WriteLine("A");
    var t = Foo();
    Console.WriteLine("B");
    await t;
    Console.WriteLine("D");

    static async Task Foo()
    {
        Console.WriteLine("C1");
        await Task.Delay(100);
        Console.WriteLine("C2");
    }
}

// ОТВЕТ: 0, 1, 2, 3. await на завершённой задаче не приостанавливает, Foo целиком проходит внутри вызова.
static async Task Task2()
{
    Console.WriteLine("0");
    var t = Foo();
    Console.WriteLine("3");
    await t;

    static async Task Foo()
    {
        Console.WriteLine("1");
        await Task.CompletedTask;
        Console.WriteLine("2");
    }
}

// ОТВЕТ: чаще всего 3 3 3: лямбды захватывают одну переменную i на весь цикл for.
static async Task Task3()
{
    var tasks = new List<Task>();
    for (int i = 0; i < 3; i++)
        tasks.Add(Task.Run(() => Console.WriteLine(i)));
    await Task.WhenAll(tasks);

    Console.WriteLine("исправлено (копия на итерацию), порядок любой:");
    tasks.Clear();
    for (int i = 0; i < 3; i++)
    {
        int copy = i;
        tasks.Add(Task.Run(() => Console.WriteLine(copy)));
    }
    await Task.WhenAll(tasks);
}

// ОТВЕТ: «после вызова», затем x. Исключение лежит в задаче до await.
static async Task Task4()
{
    var t = Boom();
    Console.WriteLine("после вызова");
    try { await t; } catch (Exception e) { Console.WriteLine(e.Message); }

    static async Task Boom() { throw new Exception("x"); }
}

// ОТВЕТ: 10 примерно за 100 мс (обе задачи запущены до первого await); второй вариант — 10 примерно за 200 мс.
static async Task Task5()
{
    var sw = Stopwatch.StartNew();
    var a = Get();
    var b = Get();
    Console.WriteLine(await a + await b);
    Console.WriteLine($"{sw.ElapsedMilliseconds} мс");

    sw.Restart();
    Console.WriteLine(await Get() + await Get());
    Console.WriteLine($"{sw.ElapsedMilliseconds} мс");

    static async Task<int> Get()
    {
        await Task.Delay(100);
        return 5;
    }
}

// ОТВЕТ: finally, затем inner.
static async Task Task6()
{
    try { await Run(); } catch (Exception e) { Console.WriteLine(e.Message); }

    static async Task Run()
    {
        try { await Task.Delay(10); throw new InvalidOperationException("inner"); }
        finally { Console.WriteLine("finally"); }
    }
}

// ОТВЕТ: дедлок. Wait блокирует единственный поток контекста, а продолжение GetAsync ждёт именно его.
static Task Task7()
{
    var ui = new SingleThreadSyncContext();
    ui.Post(async _ =>
    {
        var t = GetAsync();
        if (t.Wait(TimeSpan.FromSeconds(2)))
            Console.WriteLine(t.Result);
        else
            Console.WriteLine("(прошло 2 секунды, результата нет) — дедлок");

        // Исправление 1: обработчик async, ждём через await.
        Console.WriteLine($"await: {await GetAsync()}");

        // Исправление 2 (если блокировать всё же приходится): ConfigureAwait(false) внутри.
        var t2 = GetNoCtxAsync();
        Console.WriteLine($"ConfigureAwait(false) + Wait: завершилось={t2.Wait(TimeSpan.FromSeconds(2))}");

        ui.Complete();
    }, null);
    ui.RunOnCurrentThread();
    return Task.CompletedTask;

    static async Task<int> GetAsync() { await Task.Delay(100); return 1; }
    static async Task<int> GetNoCtxAsync() { await Task.Delay(100).ConfigureAwait(false); return 1; }
}

// ОТВЕТ: до SetResult, продолжение, после SetResult: без контекста продолжение выполняется прямо внутри SetResult (глава 5).
// С RunContinuationsAsynchronously порядок «после SetResult» и «продолжение» не определён: продолжение уходит в пул
// и соревнуется с печатью (в 12 запусках: 8 раз «продолжение» первым, 4 раза «после SetResult» первым).
static async Task Task8()
{
    var tcs = new TaskCompletionSource<int>();
    var waiter = Waiter(tcs.Task);
    Console.WriteLine("до SetResult");
    tcs.SetResult(1);
    Console.WriteLine("после SetResult");
    await waiter;

    Console.WriteLine("с RunContinuationsAsynchronously:");
    var tcs2 = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
    var waiter2 = Waiter(tcs2.Task);
    Console.WriteLine("до SetResult");
    tcs2.SetResult(1);
    Console.WriteLine("после SetResult");
    await waiter2;

    static async Task Waiter(Task<int> t) { await t; Console.WriteLine("продолжение"); }
}

// ОТВЕТ: Value = 1. Запись в AsyncLocal внутри вызываемого async-метода не видна вызывающему (глава 7).
static async Task Task9()
{
    var local = new AsyncLocal<int> { Value = 1 };
    await Inc(local);
    Console.WriteLine($"Value = {local.Value}");

    static async Task Inc(AsyncLocal<int> a) { a.Value = 2; await Task.Yield(); }
}

// ОТВЕТ: await бросает только первое исключение («первая»), но в задаче лежат оба (глава 8).
static async Task Task10()
{
    var all = Task.WhenAll(Fail("первая", 20), Fail("вторая", 50));
    try { await all; }
    catch (Exception e) { Console.WriteLine($"поймали: {e.Message}; в задаче исключений: {all.Exception!.InnerExceptions.Count}"); }

    static async Task Fail(string msg, int delay) { await Task.Delay(delay); throw new InvalidOperationException(msg); }
}

// ОТВЕТ: C, B, A. Обработчики отмены вызываются в обратном порядке регистрации (глава 9).
static Task Task11()
{
    using var cts = new CancellationTokenSource();
    cts.Token.Register(() => Console.WriteLine("A"));
    cts.Token.Register(() => Console.WriteLine("B"));
    cts.Token.Register(() => Console.WriteLine("C"));
    cts.Cancel();
    return Task.CompletedTask;
}

// ОТВЕТ: почти 0 мс. StartNew с async-лямбдой вернул Task<Task>; await ждёт только внешнюю задачу, то есть запуск лямбды (глава 10).
// Исправление: await t.Unwrap() или Task.Run.
static async Task Task12()
{
    var sw = Stopwatch.StartNew();
    var t = Task.Factory.StartNew(async () => { await Task.Delay(300); });
    await t;
    Console.WriteLine($"await t закончился через {sw.ElapsedMilliseconds} мс; тип t: {t.GetType().Name}");

    sw.Restart();
    await Task.Run(async () => { await Task.Delay(300); });
    Console.WriteLine($"Task.Run: {sw.ElapsedMilliseconds} мс");
}

sealed class SingleThreadSyncContext : SynchronizationContext
{
    private readonly BlockingCollection<(SendOrPostCallback cb, object? state)> _queue = new();
    public override void Post(SendOrPostCallback d, object? state) => _queue.Add((d, state));
    public void Complete() => _queue.CompleteAdding();

    public void RunOnCurrentThread()
    {
        var previous = Current;
        SetSynchronizationContext(this);
        foreach (var (cb, state) in _queue.GetConsumingEnumerable())
            cb(state);
        SetSynchronizationContext(previous);
    }
}