Wednesday, July 9, 2014

Тонкости использования async и await

Сегодня мы рассмотрим продолжение темы предыдущей статьи об использовании нововведений в .NET Framework 4.5, таких как ключевые операторов async/await. Сегодня мы рассмотрим способ использования async без явного применения ключевого слова await. Часть информации, которая будут приведена в данной статье, базируется на собственных знаниях, дебаге, разбора кода с помощью .NET Reflector и ILSpy, а также на видеоуроке гуру в области разработки на языке C#, топ-автора на stackoverflow.com, легендарного Джона Скита с его видеокурсом с pluralsight "AsynchronousC# 5.0". 
Небольшое отступление по видеокурсу Джона Скита. Многообещающий курс легендарного разработчика не оправдывает больших ожиданий, заслуживая, по моей субъективной оценке, твердое "удовлетворительно". Для демонстрации принципа работы автор рассматривает пример, который объясняет на протяжении пяти глав. Полезной информацией наполнено только треть видеокурса, остальное время сводится к пустому многословию о неких возможностях, которые демонстрируются по несколько раз, из-за чего зрителя невольно тянет ко сну. Я также не согласен с примерами интерфейсов, приведенными Скитом, так как в данном случае, считаю, было бы полезнее рассказать о классе TaskAwaiter и asyncTaskMethodBuilder. Но к этому мы вернемся со временем; для понимания того, как все это работает, я приведу интерфейсы, подобные тем, которые рассмотрел в своем видео Скит. Мы рассмотрим работы с async/await на самом примитивном уровне. Создадим для демонстрации примера новое консольное приложение Deep AsynchronousSample, указав при создании использование .NET Framework 4.5.
Давайте для начала рассмотрим пример, который я демонстрировал в своей предыдущей статье.
class Program
{
    static void Main(string[] args)
    {
        GetData();
        Console.ReadLine();
    }

    public static async Task<Data> GetData()
    {
        var result = await GetTaskResult();
        Console.WriteLine("Get Result {0}", result);
        return new Data { Result = 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;
    }
}

public class Data
{
    public long Result { get; set; }

}
Выше приведен простой пример, который считает сумму последовательности чисел от 0 до 10 млн. После запуска приложения мы увидим на экране следующий результат:
Более детально остановимся на разборе метода GetData(), который использует ключевые слова async/await.
public static async Task<Data> GetData()
{
    var result = await GetTaskResult();
    Console.WriteLine("Get Result {0}", result);
    return new Data { Result = result };
}
Давайте копнем этот метод чуть более глубже. В первую очередь, интересный факт: почему у нас для Task-ов работает await? Если вы знаете ответ на этот вопрос, огромный плюс. В противном случае мы сейчас рассмотрим более детально, почему это работает. Если посмотреть внимательно на класс Task и Task<T>, то можно увидеть метод, который возвращает класс TaskAwaiter.
//
// Summary:
//     Gets an awaiter used to await this System.Threading.Tasks.Task<TResult>.
//
// Returns:
//     An awaiter instance.
[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]

public TaskAwaiter<TResult> GetAwaiter();
Сам класс TaskAwaiter<T> имеет следующий вид:
public struct TaskAwaiter<TResult> : ICriticalNotifyCompletion, INotifyCompletion
{
    public bool IsCompleted { get; }

    [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
    public TResult GetResult();

    [SecuritySafeCritical]
    public void OnCompleted(Action continuation);

    [SecurityCritical]
    public void UnsafeOnCompleted(Action continuation);
}
И, соответственно, non generic версия данного класса:
public struct TaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion
{
    public bool IsCompleted { get; }

    [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
    public void GetResult();

    [SecuritySafeCritical]
    public void OnCompleted(Action continuation);

    [SecurityCritical]
    public void UnsafeOnCompleted(Action continuation);
}
То есть, по сути, вместо использования ключевого слова, мы можем переписать наш код следующим образом:
static void Main(string[] args)
{
    CulculateData();
    Console.ReadLine();
}

public static void CulculateData()
{
    TaskAwaiter<long> awaitable = GetTaskResult().GetAwaiter();
    Action moveNext = () => Console.WriteLine("Get Result {0}", awaitable.GetResult());
    awaitable.OnCompleted(moveNext);

}
Я специально не брал в расчет возвращаемый тип Task<Data>, для того чтобы упростить пример. Полный пример можно симулировать подобным образом.
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;
}
Подобный код нам генерирует сам компилятор языка C#, только для перехода там используются goto переходы. Пока вроде нет ничего сложного в рассмотренном коде (приведу этот метод более детально).
public static void CulculateData()
{
    TaskAwaiter<long> awaitable = GetTaskResult().GetAwaiter();
    Action moveNext = () => Console.WriteLine("Get Result {0}", awaitable.GetResult());
    awaitable.OnCompleted(moveNext);
}
В данном методе мы использовали класс GetAwaiter и просто передали в метод OnCompleted событие, которое будет вызвано, когда наша операция будет выполнена. Чем-то напоминает callback вызовы. С классом TaskAwaiter есть несколько интересных особенностей. Компилятор при встрече слова await использует утиную типизацию для поиска кандидатов для вызовов, генерируемых в конечном автомате. Если хорошенько подумать, то если мы реализуем свой тип данных, который будет возвращать подходящую сигнатуру, мы можем использовать ключевое слово await для типов, которые не являются тасками. В данном случае в качестве примера рассмотрим интерфейсы, имлементация которых (или реализацию каких методов в своих классах) у нас позволит провернуть такие действия для типов данных, которые не являются тасками.
public interface IAwaitable<T>
{
    IAwaiter<T> GetAwaiter();
}

public interface IAwaiter<T>
{
    bool IsComplete { get; }
    void OnCompleted(Action continuation);

    T GetResult();
}

public interface IAwaitable
{
    IAwaiter GetAwaiter();
}

public interface IAwaiter
{
    bool IsComplete { get; }
    void OnCompleted(Action continuation);

    object GetResult();
}
Вот краткий список таких интерфейсов. Но если посмотреть на класс TaskAwaiter, то можем использовать два типа интерфейсов: ICriticalNotifyComletion и INotifyCompletion. Так можем привести окончательный вид нашего интерфейса IAwaiter.
public interface IAwaiter<T> : ICriticalNotifyCompletion
{
    bool IsComplete { get; }
    T GetResult();
}
А теперь давайте рассмотрим это все на примере.
class Program
{
    static void Main(string[] args)
    {
        DummyAsyncTask();
        Console.ReadLine();
    }

    private static async Task DummyAsyncTask()
    {
        DummyAwaitable awaitable = new DummyAwaitable();
        string text = await awaitable;
        Console.WriteLine("Test {0}", text);
    }
}

public class DummyAwaitable
{
    public DummyAwaiter GetAwaiter()
    {
        return new DummyAwaiter();
    }
}

public class DummyAwaiter : INotifyCompletion
{
    public bool IsCompleted { get { return true; } }

    public void OnCompleted(Action continuation)
    {
        Console.WriteLine("This wan't get called ");
    }

    public string GetResult()
    {
        return "Surprise!";
    }
}
Вот краткий список В нашем примере мы реализовали в классе DummyAwailable метод GetAwailable, которым мы возвращаем экземпляр класса DummyAwaiter, который реализует подобные методы, как в классе TaskAwaiter, который идет в .NET Framework 4.5. Если мы запустим пример, то убедимся в том, что этот подход работает.
Обратите внимание: метод OnCompleted не был вызван потому, что свойство IsCompleted возвращает нам true, что интерпретируется как завершение выполнения метода. Чтобы убедиться в том, что метод OnCompleted будет вызван, измените значение свойства IsCompleted на false.
Еще одним интересным способом достижения такого результата является использование методов расширений (extension methods). Напишем простой пример:
class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();
    }

    private static async void DummyTestFunction()
    {
        await new DummyTest();
    }
}

public class DummyTest
{

}
Для того чтобы это пример скомпилировался нам нужно написать необходимый extension метод. Например, вот такой.
public static class DummyTestEx
{
    public static TaskAwaiter GetAwaiter(this DummyTest t)
    {
        return new TaskAwaiter();
    }
}
На этой ноте я, пожалуй, буду заканчивать статью. Надеюсь вам понравился экскурс в мир async и await. Если у вас остались какие-то вопросы поэтому буду рад на них ответить. 

No comments:

Post a Comment