Sunday, July 6, 2014

Under the hood: async/await

В этой статье мы поговорим об очень интересной теме, с которой у многих разработчиков возникают трудности в понимании. Часть разработчиков не понимает эту тему из-за специфики своей работы и так как не стыкались с ней, другие просто не задавались вопросом, почему происходит так или иначе, и как вообще это работает. Сегодня мы обсудим использование новых ключевых слов, которые появились в .Net Framework 4.5 – async/await. Эти ключевые слова добавлены для упрощения асинхронного программирования. Они скрывают некоторые сложности, которые мы можем упустить из виду, что, соответственно, принесет в наш код дополнительные проблемы. С помощью ключевых слов async/await создавать асинхронные вызовы становится так же легко, как и синхронные. Давайте создадим тестовый пример AsynchronousSample, в котором проемонстрируем наши примеры.
Не забудьте при создании выбрать .Net Framework 4.5, так как async/await появились именно там.
class Program
{
    static void Main(string[] args)
    {
        Action a = () =>
            {
                var task = GetTaskResult();
                task.Wait();
                Console.WriteLine("Get Result {0}", task.Result);
                    
            };

        a();
        Console.ReadLine();
    }

    public static Task<long> GetTaskResult()
    {
        var task = Task<long>.Factory.StartNew(() =>
            {
                long counter = 0;
                for (int i = 0; i < 10000000; i++)
                {
                    DateTime.Now.ToString(); //special code calculate this counter more slowly  
                    counter += i;
                }
                return counter;
            });

        return task;
    }
}
Выше я написал простенький пример, который высчитывает сумму последовательности чисел в 10М. Для того чтобы дождаться выполнения этого кода, мы вызываем метод,
task.Wait();
который говорит нашему коду ожидать завершение таска, для того чтобы можно было бы получить результат. Если мы будем запускать такой код в UI-приложении, написанном, например, на WPF и WindowsForms, то указанный код приведет к блокировке потока, результат которого мы ожидаем. Естественно это отразится на основном потоке, так как ожидание завершения запущенного таска мы ожидаем в главном потоке. А теперь представьте ситуацию, что нам нужно выполнить несколько таких методов один за другим?
static void Main(string[] args)
{
    Action a = () =>
    {
        var task = GetTaskResult();
        task.Wait();
        Console.WriteLine("Get Result {0}", task.Result);

        var task2 = GetTaskResult();
        task2.Wait();
        Console.WriteLine("Get Result Task2 {0}", task2.Result);

        var task3 = GetTaskResult();
        task3.Wait();
        Console.WriteLine("Get Result Task 3 {0}", task3.Result);
    };

    a();
    Console.ReadLine();
}
Код и сам по себе читается непросто, не говоря уже о выполнении. Теперь немного усложним задачу. Добавим класс Data, в котором будем хранить результат подсчета нашей последовательности, и будем возвращать этот метод сразу, как получим результат от метода GetTaskResult.
public class Data
{
    public long Result { get; set; }
}
Для этого мы можем воспользоваться способом с Wait, но воспользуемся вторым методом, который позволит сделать, по сути, то же самое – это метод ContinueWith. Давайте посмотрим на то, как изменится наш код в данном случае.
static void Main(string[] args)
{
    Action a = () =>
    {
        var task = GetData();
        task.Wait();
        Console.WriteLine("Get Result {0}", task.Result.Result);
    };

    a();
    Console.ReadLine();
}

public static Task<Data> GetData()
{
    return GetTaskResult().ContinueWith(t => new Data { Result = t.Result });
}

public static Task<long> GetTaskResult()
{
    var task = Task<long>.Factory.StartNew(() =>
    {
        long counter = 0;
        for (int i = 0; i < 10000000; i++)
        {
            DateTime.Now.ToString(); //special code calculate this counter more slowly 
            counter += i;
        }
        return counter;
    });

    return task;
}
Основная сточка кода, которая возвращает нужный результат,  это метод GetData.
public static Task<Data> GetData()
{
    return GetTaskResult().ContinueWith(t => new Data { Result = t.Result });
}
Прежде чем перейти к теории, перепишу код с использованием ключевых слов async/await, а затем расскажу о ключевых отличиях.
static void Main(string[] args)
{
    Action a = async () =>
    {
        var data = await GetData();
        Console.WriteLine("Get Result {0}", data.Result);
    };

    a();
    Console.ReadLine();
}

public static async Task<Data> GetData()
{
    var result = await GetTaskResult();
    return new Data { Result = result };
}
Метод GetTaskResult остается без изменений. Изменения затронули мой Action a, а также метод GetData. Что этот метод, что предыдущей имеет накладные расходы. В этих двух подходах есть существенное отличие.
  • Task строит цепочку продолжений, которая увеличивается в соответствии с количеством задач, связанных последовательно, и состояние системы управляется через замыкания, найденные компилятором.
  • Async/await строит машину состояний, которая не использует дополнительных ресурсов при добавлении новых шагов. Однако компилятор может определить больше переменных для сохранение в стеки машины состояний, в зависимости от вашего кода (и компилятора).
Давайте посмотрим, как же выглядит это на уровне IL-кода. Первым делом мы посмотрим, какой код у нас сгенерирует компилятор языка C# для кода без ключевых слов async/await.
.method public hidebysig static
        class [mscorlib]System.Threading.Tasks.Task`1<int64> GetTaskResult () cil managed
    {
        // Method begins at RVA 0x2170
        // Code size 49 (0x31)
        .maxstack 3
        .locals init (
            [0] class [mscorlib]System.Threading.Tasks.Task`1<int64> task,
            [1] class [mscorlib]System.Threading.Tasks.Task`1<int64> CS$1$0000
        )

        IL_0000: nop
        IL_0001: call class [mscorlib]System.Threading.Tasks.TaskFactory`1<!0> class [mscorlib]System.Threading.Tasks.Task`1<int64>::get_Factory()
        IL_0006: ldsfld class [mscorlib]System.Func`1<int64> AsynchronousSample.Program::'CS$<>9__CachedAnonymousMethodDelegate6'
        IL_000b: brtrue.s IL_0020

        IL_000d: ldnull
        IL_000e: ldftn int64 AsynchronousSample.Program::'<GetTaskResult>b__5'()
        IL_0014: newobj instance void class [mscorlib]System.Func`1<int64>::.ctor(objectnative int)
        IL_0019: stsfld class [mscorlib]System.Func`1<int64> AsynchronousSample.Program::'CS$<>9__CachedAnonymousMethodDelegate6'
        IL_001e: br.s IL_0020

        IL_0020: ldsfld class [mscorlib]System.Func`1<int64> AsynchronousSample.Program::'CS$<>9__CachedAnonymousMethodDelegate6'
        IL_0025: callvirt instance class [mscorlib]System.Threading.Tasks.Task`1<!0> class [mscorlib]System.Threading.Tasks.TaskFactory`1<int64>::StartNew(class [mscorlib]System.Func`1<!0>)
        IL_002a: stloc.0
        IL_002b: ldloc.0
        IL_002c: stloc.1
        IL_002d: br.s IL_002f

        IL_002f: ldloc.1
        IL_0030: ret
    } // end of method Program::GetTaskResult
В коде нет ничего такого, что могло бы вызвать страх. Код очень подобный тому, который у нас в языке C#, за исключением CachedAnonymousMethodDelegate, которым в IL-коде обозначается создание анонимного метода.
Теперь посмотрим тот же код, который у нас будет сгенерирован с ключевыми словами async/await.
.method public hidebysig static
    class [mscorlib]System.Threading.Tasks.Task`1<class AsynchronousSample.Data> GetData () cil managed
{
    .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
        01 00 28 41 73 79 6e 63 68 72 6f 6e 6f 75 73 53
        61 6d 70 6c 65 2e 50 72 6f 67 72 61 6d 2b 3c 47
        65 74 44 61 74 61 3e 64 5f 5f 37 00 00
    )
    // Method begins at RVA 0x22dc
    // Code size 54 (0x36)
    .maxstack 2
    .locals init (
        [0] valuetype AsynchronousSample.Program/'<GetData>d__7',
        [1] class [mscorlib]System.Threading.Tasks.Task`1<class AsynchronousSample.Data>,
        [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>
    )

    IL_0000: ldloca.s 0
    IL_0002: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>::Create()
    IL_0007: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> AsynchronousSample.Program/'<GetData>d__7'::'<>t__builder'
    IL_000c: ldloca.s 0
    IL_000e: ldc.i4.m1
    IL_000f: stfld int32 AsynchronousSample.Program/'<GetData>d__7'::'<>1__state'
    IL_0014: ldloca.s 0
    IL_0016: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> AsynchronousSample.Program/'<GetData>d__7'::'<>t__builder'
    IL_001b: stloc.2
    IL_001c: ldloca.s 2
    IL_001e: ldloca.s 0
    IL_0020: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>::Start<valuetype AsynchronousSample.Program/'<GetData>d__7'>(!!0&)
    IL_0025: ldloca.s 0
    IL_0027: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> AsynchronousSample.Program/'<GetData>d__7'::'<>t__builder'
    IL_002c: call instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>::get_Task()
    IL_0031: stloc.1
    IL_0032: br.s IL_0034

    IL_0034: ldloc.1
    IL_0035: ret
// end of method Program::GetData
В этом коде мы уже можем увидеть ключевое отличие. Это появление класса AsyncTaskMethodBuilder. Также добавляется код самой машины состояний.
.class nested private auto ansi sealed beforefieldinit '<GetData>d__7'
    extends [mscorlib]System.ValueType
    implements [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
    .field public int32 '<>1__state'
    .field public valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> '<>t__builder'
    .field public int64 '<result>5__8'
    .field public class AsynchronousSample.Data '<>g__initLocal6'
    .field private valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64> '<>u__$awaiter9'
    .field private object '<>t__stack'

    // Methods
    .method private final hidebysig newslot virtual
        instance void MoveNext () cil managed
    {
        .override method instance void [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
        // Method begins at RVA 0x21c8
        // Code size 232 (0xe8)
        .maxstack 3
        .locals init (
            [0] bool '<>t__doFinallyBodies',
            [1] class AsynchronousSample.Data '<>t__result',
            [2] class [mscorlib]System.Exception '<>t__ex',
            [3] int32 CS$4$0000,
            [4] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64> CS$0$0001,
            [5] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64> CS$0$0002,
            [6] int64 CS$0$0003
        )

        .try
        {
            IL_0000: ldc.i4.1
            IL_0001: stloc.0
            IL_0002: ldarg.0
            IL_0003: ldfld int32 AsynchronousSample.Program/'<GetData>d__7'::'<>1__state'
            IL_0008: stloc.3
            IL_0009: ldloc.3
            IL_000a: ldc.i4.s -3
            IL_000c: beq.s IL_0014

            IL_000e: ldloc.3
            IL_000f: ldc.i4.0
            IL_0010: beq.s IL_0019

            IL_0012: br.s IL_001b

            IL_0014: br IL_00b6

            IL_0019: br.s IL_0058

            IL_001b: br.s IL_001d

            IL_001d: nop
            IL_001e: call class [mscorlib]System.Threading.Tasks.Task`1<int64> AsynchronousSample.Program::GetTaskResult()
            IL_0023: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class [mscorlib]System.Threading.Tasks.Task`1<int64>::GetAwaiter()
            IL_0028: stloc.s CS$0$0001
            IL_002a: ldloca.s CS$0$0001
            IL_002c: call instance bool valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64>::get_IsCompleted()
            IL_0031: brtrue.s IL_0077

            IL_0033: ldarg.0
            IL_0034: ldc.i4.0
            IL_0035: stfld int32 AsynchronousSample.Program/'<GetData>d__7'::'<>1__state'
            IL_003a: ldarg.0
            IL_003b: ldloc.s CS$0$0001
            IL_003d: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64> AsynchronousSample.Program/'<GetData>d__7'::'<>u__$awaiter9'
            IL_0042: ldarg.0
            IL_0043: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> AsynchronousSample.Program/'<GetData>d__7'::'<>t__builder'
            IL_0048: ldloca.s CS$0$0001
            IL_004a: ldarg.0
            IL_004b: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>::AwaitUnsafeOnCompleted<valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64>, valuetype AsynchronousSample.Program/'<GetData>d__7'>(!!0&, !!1&)
            IL_0050: nop
            IL_0051: ldc.i4.0
            IL_0052: stloc.0
            IL_0053: leave IL_00e6

            IL_0058: ldarg.0
            IL_0059: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64> AsynchronousSample.Program/'<GetData>d__7'::'<>u__$awaiter9'
            IL_005e: stloc.s CS$0$0001
            IL_0060: ldarg.0
            IL_0061: ldloca.s CS$0$0002
            IL_0063: initobj valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64>
            IL_0069: ldloc.s CS$0$0002
            IL_006b: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64> AsynchronousSample.Program/'<GetData>d__7'::'<>u__$awaiter9'
            IL_0070: ldarg.0
            IL_0071: ldc.i4.m1
            IL_0072: stfld int32 AsynchronousSample.Program/'<GetData>d__7'::'<>1__state'

            IL_0077: ldloca.s CS$0$0001
            IL_0079: call instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64>::GetResult()
            IL_007e: ldloca.s CS$0$0001
            IL_0080: initobj valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int64>
            IL_0086: stloc.s CS$0$0003
            IL_0088: ldarg.0
            IL_0089: ldloc.s CS$0$0003
            IL_008b: stfld int64 AsynchronousSample.Program/'<GetData>d__7'::'<result>5__8'
            IL_0090: ldarg.0
            IL_0091: newobj instance void AsynchronousSample.Data::.ctor()
            IL_0096: stfld class AsynchronousSample.Data AsynchronousSample.Program/'<GetData>d__7'::'<>g__initLocal6'
            IL_009b: ldarg.0
            IL_009c: ldfld class AsynchronousSample.Data AsynchronousSample.Program/'<GetData>d__7'::'<>g__initLocal6'
            IL_00a1: ldarg.0
            IL_00a2: ldfld int64 AsynchronousSample.Program/'<GetData>d__7'::'<result>5__8'
            IL_00a7: callvirt instance void AsynchronousSample.Data::set_Result(int64)
            IL_00ac: nop
            IL_00ad: ldarg.0
            IL_00ae: ldfld class AsynchronousSample.Data AsynchronousSample.Program/'<GetData>d__7'::'<>g__initLocal6'
            IL_00b3: stloc.1
            IL_00b4: leave.s IL_00d0

            IL_00b6: leave.s IL_00d0
        } // end .try
        catch [mscorlib]System.Exception
        {
            IL_00b8: stloc.2
            IL_00b9: ldarg.0
            IL_00ba: ldc.i4.s -2
            IL_00bc: stfld int32 AsynchronousSample.Program/'<GetData>d__7'::'<>1__state'
            IL_00c1: ldarg.0
            IL_00c2: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> AsynchronousSample.Program/'<GetData>d__7'::'<>t__builder'
            IL_00c7: ldloc.2
            IL_00c8: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>::SetException(class [mscorlib]System.Exception)
            IL_00cd: nop
            IL_00ce: leave.s IL_00e6
        } // end handler

        IL_00d0: nop
        IL_00d1: ldarg.0
        IL_00d2: ldc.i4.s -2
        IL_00d4: stfld int32 AsynchronousSample.Program/'<GetData>d__7'::'<>1__state'
        IL_00d9: ldarg.0
        IL_00da: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> AsynchronousSample.Program/'<GetData>d__7'::'<>t__builder'
        IL_00df: ldloc.1
        IL_00e0: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>::SetResult(!0)
        IL_00e5: nop

        IL_00e6: nop
        IL_00e7: ret
    } // end of method '<GetData>d__7'::MoveNext

    .method private final hidebysig newslot virtual
        instance void SetStateMachine (
            class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine param0
        ) cil managed
    {
        .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = (
            01 00 00 00
        )
        .override method instance void [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine)
        // Method begins at RVA 0x22cc
        // Code size 13 (0xd)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data> AsynchronousSample.Program/'<GetData>d__7'::'<>t__builder'
        IL_0006: ldarg.1
        IL_0007: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<class AsynchronousSample.Data>::SetStateMachine(class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine)
        IL_000c: ret
    } // end of method '<GetData>d__7'::SetStateMachine

// end of class <GetData>d__7

Первое впечатление от этого кода может вызвать приступ депрессии, потому что в нем можно здорово сломать голову.  В этом плане IL-код смотреть напрямую очень сложно и  из ILSpy в данном случае плохой помощник. Если смотреть через ILSpy, то данный код не показывает сгенерированные компилятором методы, что очень плохо. В данном случае нам лучше подойдет .NET Reflector, который лучше показывает, что происходит внутри. 
[CompilerGenerated]
private struct <GetData>d__7 : IAsyncStateMachine
{
    // Fields
    public int <>1__state;
    public Data <>g__initLocal6;
    public AsyncTaskMethodBuilder<Data> <>t__builder;
    private object <>t__stack;
    private TaskAwaiter<long> <>u__$awaiter9;
    public long <result>5__8;

    // Methods
    private void MoveNext();
    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine param0);
}
Рефлектор показывает, что у нас была сгенерирована следующая структура данных, которая наследуется от интерфейса IAsyncStateMachine. Интерфейс IAsyncStateMachine имеет два метода: MoveNext и SetStateMachine. Если мы раскроем полностью эту структуру, то увидим следующий сгенерированный код для методов MoveNext и SetStateMachine:
[CompilerGenerated]
private struct <GetData>d__7 : IAsyncStateMachine
{
    public int <>1__state;
    public Data <>g__initLocal6;
    public AsyncTaskMethodBuilder<Data> <>t__builder;
    private object <>t__stack;
    private TaskAwaiter<long> <>u__$awaiter9;
    public long <result>5__8;

    private void MoveNext()
    {
        Data data;
        try
        {
            TaskAwaiter<long> awaiter;
            bool flag = true;
            switch (this.<>1__state)
            {
                case -3:
                    goto Label_00D0;

                case 0:
                    break;

                default:
                    awaiter = Program.GetTaskResult().GetAwaiter();
                    if (awaiter.IsCompleted)
                    {
                        goto Label_0077;
                    }
                    this.<>1__state = 0;
                    this.<>u__$awaiter9 = awaiter;
                    this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<long>, Program.<GetData>d__7>(ref awaiter, ref this);
                    flag = false;
                    return;
            }
            awaiter = this.<>u__$awaiter9;
            this.<>u__$awaiter9 = new TaskAwaiter<long>();
            this.<>1__state = -1;
        Label_0077:
            long introduced7 = awaiter.GetResult();
            awaiter = new TaskAwaiter<long>();
            long num2 = introduced7;
            this.<result>5__8 = num2;
            this.<>g__initLocal6 = new Data();
            this.<>g__initLocal6.Result = this.<result>5__8;
            data = this.<>g__initLocal6;
        }
        catch (Exception exception)
        {
            this.<>1__state = -2;
            this.<>t__builder.SetException(exception);
            return;
        }
    Label_00D0:
        this.<>1__state = -2;
        this.<>t__builder.SetResult(data);
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine param0)
    {
        this.<>t__builder.SetStateMachine(param0);
    }

}
Обратите внимание на использование метода MoveNext и goto для перехода к состояниям. По сути, нам компилятор сгенерил обычный switch и переход между состояниями машины с помощью goto. Постараюсь объяснить, как это работает, на примере напрямую в языке C#. Этот код будет подобен тому, который генерит компилятор языка C#, за исключением того, что я его упростил по максимуму и не использовал goto для перехода. После этого код стал более понятным и читабельным. Давайте создадим новый метод GetStateMachineData, в котором реализуем поведение, подобное тому, которое нам строит компилятор языка.
public static Task<Data> GetStateMachineData()
{
    var builder = new AsyncTaskMethodBuilder<Data>();
    int state = 0;
    Action moveNext = null;
    TaskAwaiter<long> awaiter = new TaskAwaiter<long>();

    moveNext = () =>
    {

        try
        {
            if (state == 1)
            {
                builder.SetResult(new Data { Result = awaiter.GetResult() });
                return;
            }


            try
            {
                awaiter = GetTaskResult().GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    state = 1;
                    awaiter.OnCompleted(moveNext);
                    return;
                }

                builder.SetResult(new Data { Result = awaiter.GetResult() });
            }
            catch (Exception) { }
            builder.SetResult(null);
        }
        catch (Exception exception)
        {
            builder.SetException(exception);
        }
    };

    moveNext();
    return builder.Task;

}
Использование этого метода ничем не отличается от предыдущих вариантов с Wait.
static void Main(string[] args)
{
    Action action = () =>
    {
        var task = GetStateMachineData();
        task.Wait();
        Console.WriteLine(task.Result.Result);

    };

    action();
    Console.ReadLine();

}
Если вы внимательно посмотрите на метод GetStateMachineData, то увидите, что я его, по сути, вызываю дважды. Первый раз вызываю этот action в конце метода, затем передаю в созданный мной класс TaskAwaiter в метод OnComplete, который будет вызван, когда мы получим результат выполнения подсчета суммы чисел.
Вы могли иногда стыкаться с таким кодом, когда пытались при использовании ключевых слов async/await использовать ожидание таска, например, через метод Wait, и ваш интерфейс попросту подвисал. Почему же не подвисало мое приложение? Потому что я не использовал UI, который можете использовать вы в своих примерах.
Код .NET всегда исполняется в некотором контексте. Этот контекст определяет текущего пользователя и другие значения, требуемые фреймворком. В некоторых контекстах выполнения код работает в контексте синхронизации, который управляет выполнением задач и другой асинхронной работы.
По-умолчанию после await код продолжит работать в контексте, в котором он был запущен. Это удобно, потому что, в основном, вы захотите, чтобы контекст безопасности был восстановлен, а также чтобы ваш код после await имел доступ к объектам Windows UI, если он уже имел доступ к ним при старте.
Некоторые контексты синхронизации не поддерживают повторный вход в них и являются однопоточными. Это означает, что только одна единица работы может выполняться в этом контексте одновременно. Примером этого может быть поток Windows UI или контекст ASP.NET.
В таких однопоточных контекстах синхронизации довольно легко получить deadlock. Если вы создадите задачу в однопоточном контексте и потом будете ждать в этом же контексте, ваш код, который ждёт, будет блокировать выполнение фоновой задачи.
Например, если мы запустим следующий код в Windows UI, то можем запросто получить deadlock.
static void Main(string[] args)
{
    Action action = () =>
    {
        var task = GetData().Result;
        Console.WriteLine(task.Result);

    };

    action();
    Console.ReadLine();
}

public static async Task<Data> GetData()
{
    var result = await GetTaskResult();

    return new Data { Result = result };
}
Мне очень понравилась статья Эрика Липперта "Real world async/await defects", где он продемонстрировал пример, когда deadlock может происходить явно. Код с примера с комментарием:
Frob GetFrob()
{
    Frob result = null;
    var networkDevice = new NetworkDevice();
    networkDevice.OnDownload +=
        async (state) => { result = await Frob.DeserializeStateAsync(state); };
    networkDevice.GetSerializedState(); // Synchronous
    return result;
}
Статья небольшая. Рекомендую ее прочесть, если у вас нет проблем с английским языком. Также очень полезные ссылки, которые Липперт приводит в статье, так как они позволяют глубже понять некую область.

Итоги

На этой ноте завершаю статью об использовании async/await. Надеюсь, если вы не знали раньше, как работает async/await, теперь в этом плане будет немного проще. Если у вас возникли какие-то вопросы по статье, буду рад на них ответить. 

No comments:

Post a Comment