Wednesday, November 11, 2015

Using statements with async/await in C# 7

Здравствуйте, уважаемые читатели. Сегодняшняя статья тем отличается от остальных, что имеет больше ознакомительный характер и отображает в большей мере мою субъективную точку зрения, чем обзор полезности для использования в своих проектах. Статья будет основываться на новых возможностях языка C# 7, а именно: использовании async/await для потоков Async streams and disposal
На гитхабе в ветке о рослине ведутся две дискуссии по этому поводу. Первая дискуссия о language support for async sequences не особенно интересна, потому что реализовать async для коллекции особого труда не составляет, тем более что в Rx уже такое, по сути, сделали в интерфейсе IObservable. Мне больше по душе вторая дискуссия по реализации async для IDisposable интерфейса IAsyncDisposable, using statements, and async/await. Вот об этой теме поговорим подробнее. Для того чтобы понять, как это работает, нам понадобится инструмент, например, ILSpy, для того чтобы посмотреть исходный код, а также пригодится знание принципов работы async/await изнутри. Поскольку await для блока finally появился только с C# 6.0, вы можете посмотреть на то, как работает async/await в данной версии языка изнутри, в моей статье Async и await в C# 6.0.  
Давайте рассмотрим суть, которую предлагают реализовать в новом C# 7. Основная идея заключается в использовании нового интерфейса IAsyncDisposable, задачей которого будет очистить ресурсы асинхронно, если этот интерфейс реализован. Плюс возможность использования интерфейса совместно с блоком using.
using (ResourceType resource = expression) statement
Сам интерфейс выглядит следующем образом.
public interface IAsyncDisposable : IDisposable
{
    Task DisposeAsync();
}
Давайте посмотрим, как работает обычный IDispose интерфейс. Для этого напишем простой пример, в котором продемонстрируем его работу.
class Program
{
    static void Main(string[] args)
    {
        using (var test = new Test())
        {
            WriteLine("Using block");
        }
        ReadLine();
    }
}

public class Test : IDisposable
{
    public void Dispose()
    {
        WriteLine("Dispose class test");
    }
}
После запуска мы можем увидеть на экране результат.
Если посмотреть на IL-код, то можно увидеть, что наш код – это, по сути, развёртывание ручного try-finally блока.
// Methods
    
.method private hidebysig static
        
void Main (
            
string[] args
        ) 
cil managed
    {
        
// Method begins at RVA 0x2050
        
// Code size 40 (0x28)
        
.maxstack 1
        
.entrypoint
        
.locals init (
            [0] 
class AsyncWithUsingBlock.Test
        )

        IL_0000: 
nop
        IL_0001: 
newobj instance void AsyncWithUsingBlock.Test::.ctor()
        IL_0006: 
stloc.0
        
.try
        {
            IL_0007: 
nop
            IL_0008: 
ldstr "Using block"
            IL_000d: 
call void [mscorlib]System.Console::WriteLine(string)
            IL_0012: 
nop
            IL_0013: 
nop
            IL_0014: 
leave.s IL_0021
        } 
// end .try
        
finally
        {
            IL_0016: 
ldloc.0
            IL_0017: 
brfalse.s IL_0020

            IL_0019: 
ldloc.0
            IL_001a: 
callvirt instance void [mscorlib]System.IDisposable::Dispose()
            IL_001f: 
nop

            IL_0020: 
endfinally
        } 
// end handler

        IL_0021: 
call string [mscorlib]System.Console::ReadLine()
        IL_0026: 
pop
        IL_0027: 
ret
    } 
// end of method Program::Main
То есть, если мы перепишем наш код следующим образом, то он у нас будет идентичен предыдущему.
static void Main(string[] args)
{
    var test = new Test();
    try
    {
        WriteLine("Using block");
    }
    finally
    {
        test.Dispose();
    }
    ReadLine();
}
Теперь давайте расширим наш класс Test, чтобы он реализовывал интерфейс IAsyncDisposable, и реализуем возможность языка C# 6 с использованием ключевого слова await в блоке finally.
class Program
{
    static void Main(string[] args)
    {
        TestAsyncBlock();
        ReadLine();
    }

    private static async void TestAsyncBlock()
    {
        var test = new Test();
        try
        {
            WriteLine("Using block");
        }
        finally
        {
            await test.DisposeAsync();
        }
    }
}

public class Test : IAsyncDisposable
{
    public void Dispose()
    {
        WriteLine("Dispose class test");
    }

    public Task DisposeAsync()
    {
        return Task.Factory.StartNew(() =>
        {
            WriteLine("Dispose class async test");
        });
    }
}
После запуска мы можем увидеть интересующий нас результат на экране.
В примере приведен способ также с динамической проверкой на тип данных, например, вот так:
private static async void TestAsyncBlock()
{
    var test = GenerateTestClass();
    try
    {
        WriteLine("Using block");
    }
    finally
    {
        if (test != null)
        {
            IAsyncDisposable tmp = test as IAsyncDisposable;
            if (tmp != null)
            {
                await tmp.DisposeAsync();
            }
            else
            {
                ((IDisposable)test).Dispose();
            }
        }
    }
}

private static Test GenerateTestClass()
{
    return new Test();
}
Или таким способом:
private static async void TestAsyncBlock()
{
    var test = GenerateTestClass();
    IDisposable d = (IDisposable)test;
    try
    {
        WriteLine("Using block");
    }
    finally
    {
        if (d != null)
        {
            IAsyncDisposable tmp = d as IAsyncDisposable;
            if (d != null)
            {
                await tmp.DisposeAsync();
            }
            else
            {
                d.Dispose();
            }
        }
    }
}

private static Test GenerateTestClass()
{
    return new Test();
}
Разработчик Microsoft, а именно Sam Harwell, предложил вариант с использованием Extension метода.
public static T ConfigureDispose<T>(this T disposable, bool continueOnCapturedContext)
    where T : IAsyncDisposable
Мне, к сожалению, сложно представить, как это можно реализовать, зато у меня есть другое предложение, которое совпадает с одним из участников дискуссии по данному вопросу: добавление нового ключевого слова.
async using
Тогда наш код будет выглядеть следующим образом:
async using (var test = new Test())
{
    WriteLine("Using block");
}
Во-первых, этот код не так и тяжело реализовать, но для этого понадобится делать изменения в Core .Net.
Постараюсь объяснить, почему это несложно и вполне реализуемо. Выше вы уже видели, в какую конструкцию превращается блок using(), а теперь давайте посмотрим, в какую конструкцию превращается следующий код.
private static async void TestAsyncBlock()
{
    var test = new Test();
    try
    {
        WriteLine("Using block");
    }
    finally
    {
        await test.DisposeAsync();
    }
}
Если взглянем на код, который у нас будет сгенерирован, то можно будет понять, как нам быть дальше. Для того чтобы посмотреть на то, что сгенерирует компилятор, я воспользовался .Net Reflector, потому что он намного лучше в данном плане показывает код, который генерирует компилятор.
namespace AsyncWithUsingBlock
{
    using System;
    using System.Diagnostics;
    using System.Runtime.CompilerServices;
    using System.Runtime.ExceptionServices;

    internal class Program
    {

        private static void Main(string[] args)
        {
            TestAsyncBlock();
            Console.ReadLine();
        }

        [AsyncStateMachine(typeof(< TestAsyncBlock > d__1)), DebuggerStepThrough]
        private static void TestAsyncBlock()
        {
            < TestAsyncBlock > d__1 stateMachine = new < TestAsyncBlock > d__1 {
                <> t__builder = AsyncVoidMethodBuilder.Create(),
                <> 1__state = -1
                  };
            stateMachine.<> t__builder.Start << TestAsyncBlock > d__1 > (ref stateMachine);
        }

        [CompilerGenerated]
        private sealed class <TestAsyncBlock>d__1 : IAsyncStateMachine
        {
            public int <>1__state;
            private object <>s__2;
            private int <>s__3;
            public AsyncVoidMethodBuilder<> t__builder;
            private TaskAwaiter<> u__1;
            private Test<test>5__1;

            private void MoveNext()
            {
                Exception exception;
                int num = this.<> 1__state;
                try
                {
                    object obj2;
                    TaskAwaiter awaiter;
                    if (num != 0)
                    {
                        this.< test > 5__1 = new Test();
                        this.<> s__2 = null;
                        this.<> s__3 = 0;
                        try
                        {
                            Console.WriteLine("Using block");
                        }
                        catch (object obj1)
                        {
                            obj2 = obj1;
                            this.<> s__2 = obj2;
                        }
                        awaiter = this.< test > 5__1.DisposeAsync().GetAwaiter();
                        if (!awaiter.IsCompleted)
                        {
                            this.<> 1__state = num = 0;
                            this.<> u__1 = awaiter;
                            Program.< TestAsyncBlock > d__1 stateMachine = this;
                            this.<> t__builder.AwaitUnsafeOnCompleted < TaskAwaiter, Program.< TestAsyncBlock > d__1 > (ref awaiter, ref stateMachine);
                            return;
                        }
                    }
                    else
                    {
                        awaiter = this.<> u__1;
                        this.<> u__1 = new TaskAwaiter();
                        this.<> 1__state = num = -1;
                    }
                    awaiter.GetResult();
                    awaiter = new TaskAwaiter();
                    obj2 = this.<> s__2;
                    if (obj2 != null)
                    {
                        exception = obj2 as Exception;
                        if (exception == null)
                        {
                            throw obj2;
                        }
                        ExceptionDispatchInfo.Capture(exception).Throw();
                    }
                    int num1 = this.<> s__3;
                    this.<> s__2 = null;
                }
                catch (Exception exception1)
                {
                    exception = exception1;
                    this.<> 1__state = -2;
                    this.<> t__builder.SetException(exception);
                    return;
                }
            this.<> 1__state = -2;
            this.<> t__builder.SetResult();
        }

        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }
    }
}
Компилятор нам построил обычную машину переходов, известную как Machine State паттерн. Немного отформатируем этот код к читабельному виду, чтобы разобраться, что же он подразумевает. По сути, компилятор строит следующую структуру:
try //Finally block
{
    try //Try block
    {
    }
    catch // End try block
    {
    }
}
catch //End finally block
{
}
Теперь посмотрим на тот же код, только слегка отформатированный вручную, чтобы было более-менее читабельно.
namespace AsyncWithUsingBlock
{
    using System;
    using System.Diagnostics;
    using System.Runtime.CompilerServices;
    using System.Runtime.ExceptionServices;

    public interface IAsyncDisposable : IDisposable
    {
        Task DisposeAsync();
    }

    public class Test : IAsyncDisposable
    {
        public void Dispose()
        {
            WriteLine("Dispose class test");
        }

        public Task DisposeAsync()
        {
            return Task.Factory.StartNew(() =>
            {
                WriteLine("Dispose class async test");
            });
        }
    }
    internal class Program
    {

        private static void Main(string[] args)
        {
            TestAsyncBlock();
            Console.ReadLine();
        }

        private static void TestAsyncBlock()
        {
            StateMachine stateMachine = new StateMachine();

            stateMachine._builder = AsyncVoidMethodBuilder.Create();
            stateMachine._state = -1;
            stateMachine._builder.Start(ref stateMachine);
        }

        public sealed class StateMachine : IAsyncStateMachine
        {
            public int _state;
            private Exception _exception;
            public AsyncVoidMethodBuilder _builder;
            private TaskAwaiter _awaiter;
            private Test _test;

            public void MoveNext()
            {
                Exception exception;
                int num = _state;
                try
                {
                    Exception ex2;
                    TaskAwaiter awaiter;
                    if (num != 0)
                    {
                        _test = new Test();
                        exception = null;
                        try
                        {
                            WriteLine("Using block");
                        }
                        catch (Exception ex)
                        {
                            ex2 = ex;
                            _exception = ex2;
                        }
                        awaiter = _test.DisposeAsync().GetAwaiter();
                        if (!awaiter.IsCompleted)
                        {
                            _state = num = 0;
                            _awaiter = awaiter;
                            var stateMachine = this;
                            _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                            return;
                        }
                    }
                    else
                    {
                        awaiter = _awaiter;
                        _awaiter = new TaskAwaiter();
                        _state = num = -1;
                    }
                    awaiter.GetResult();
                    awaiter = new TaskAwaiter();
                    ex2 = _exception;
                    if (ex2 != null)
                    {
                        exception = ex2 as Exception;
                        if (exception == null)
                        {
                            throw ex2;
                        }
                        ExceptionDispatchInfo.Capture(exception).Throw();
                    }
                    _exception = null;
                }
                catch (Exception ex1)
                {
                    exception = ex1;
                    _state = -2;
                    _builder.SetException(exception);
                    return;
                }
                _state = -2;
                _builder.SetResult();
            }

            public void SetStateMachine(IAsyncStateMachine stateMachine)
            {
            }
        }
    }
}
На самом деле, вся логика у нас размещена в методе MoveNext() класса StateMachine. Если внимательно присмотреться к этому коду, то можно убедиться, что он не такой уж и запутанный, но с учетом разных TaskAwaiter и строителей на примере AsyncVoidMethodBuilder этот код все равно сложно читать. Поэтому я постараюсь тот код, который генерирует компилятор, переписать в еще более ясный, но для этого нужно выбросить всю сверхкомплектную логику и сделать все с помощью самых азбучных типов данных.  Ниже представлена такая реализация.
public enum State
{
    None = 0,
    MainBlock = 1,
    CatchBlock = 2,
    FinallyBlock = 3
}
public static void TestAsyncBlock2()
{
    var state = State.None;
    Exception mainBlockException = null;
    Exception finallyBlockException = null;
    Action moveNext = null;
    var test = new Test();

    moveNext = () =>
    {
        switch (state)
        {
            case State.MainBlock: //Main Block
                {
                    try
                    {
                        WriteLine("Using block");
                    }
                    catch (Exception ex)
                    {
                        mainBlockException = ex;
                    }

                    try
                    {
                        var res = test.DisposeAsync(); // wait for complete
                        Task.WaitAll(res);

                        state = State.FinallyBlock;
                    }
                    catch(Exception ex)
                    {
                        finallyBlockException = ex;
                        state = State.FinallyBlock;
                    }
                           
                    moveNext();
                    break;
                }
            case State.FinallyBlock: // Finally block
                {
                    if(mainBlockException != null)
                    {
                        ExceptionDispatchInfo.Capture(mainBlockException).Throw();
                    }
                           
                    if(finallyBlockException != null)
                    {
                        //finally block exception
                        ExceptionDispatchInfo.Capture(mainBlockException).Throw();
                    }
                    break;
                }
            default:
                {
                    state = State.MainBlock;
                    moveNext();
                    break;
                }
        }
    };

    moveNext();
}
У нас сейчас появился блок switch, новый enum State, и функция, которая изменяет статус и вызывает себя же через Action. Результат ошибок мы сохраняем в переменные mainBlockException и finallyBlockException, для того чтобы понять, какой блок нам кинул ошибку. В старой логике было просто try-catch блок внутри блока try-catch. Так как результат нам не важен, то, выходит, у нас самый примитивный вариант реализации. А теперь давайте немого трансформируем наш вариант, "приправив" его добавлением проверки нужного типа.
public enum State
{
    None = 0,
    MainBlock = 1,
    CatchBlock = 2,
    FinallyBlock = 3
}
public static void TestAsyncBlock2()
{
    var state = State.None;
    Exception mainBlockException = null;
    Exception finallyBlockException = null;
    Action moveNext = null;
    var test = new Test();

    moveNext = () =>
    {
        switch (state)
        {
            case State.MainBlock: //Main Block
                {
                    try
                    {
                        WriteLine("Using block");
                    }
                    catch (Exception ex)
                    {
                        mainBlockException = ex;
                    }

                    try
                    {
                        IAsyncDisposable tmp = test as IAsyncDisposable;
                        if (tmp != null)
                        {
                            var res = test.DisposeAsync(); // wait for complete
                            Task.WaitAll(res);

                            state = State.FinallyBlock;
                        }
                        else
                        {
                            ((IDisposable)test).Dispose();
                        }
                        state = State.FinallyBlock;
                    }
                    catch(Exception ex)
                    {
                        finallyBlockException = ex;
                        state = State.FinallyBlock;
                    }
                           
                    moveNext();
                    break;
                }
            case State.FinallyBlock: // Finally block
                {
                    if(mainBlockException != null)
                    {
                        ExceptionDispatchInfo.Capture(mainBlockException).Throw();
                    }
                           
                    if(finallyBlockException != null)
                    {
                        //finally block exception
                        ExceptionDispatchInfo.Capture(mainBlockException).Throw();
                    }
                    break;
                }
            default:
                {
                    state = State.MainBlock;
                    moveNext();
                    break;
                }
        }
    };

    moveNext();
}
Для того чтобы проверить, что все работает, в нашем классе Test заменим интерфейс IAsyncDisposable на IDisposable.
public class Test : IDisposable
{
    public void Dispose()
    {
        WriteLine("Dispose class test");
    }

    public Task DisposeAsync()
    {
        return Task.Factory.StartNew(() =>
        {
            WriteLine("Dispose class async test");
        });
    }
}
После этого запустим нашу программу и посмотрим на результат.
Как видим, все работает достаточно успешно. То есть, мы построили Machine State паттерн для новой реализации (хотя неизвестно, как это будет генерировать компилятор).
Поскольку я излишне все упростил, то для реальной реализации на основе TaskAwaiter и AsyncVoidMethodBuilder логику нужно немного переписать.
public sealed class StateMachine : IAsyncStateMachine
{
    public int _state;
    private Exception _exception;
    public AsyncVoidMethodBuilder _builder;
    private TaskAwaiter _awaiter;
    private Test _test;
    private bool syncComplete = false;

    public void MoveNext()
    {
        Exception exception;
        int num = _state;
        try
        {
            Exception ex2;
            TaskAwaiter awaiter;
            if (num != 0)
            {
                _test = new Test();
                exception = null;
                try
                {
                    WriteLine("Using block");
                }
                catch (Exception ex)
                {
                    ex2 = ex;
                    _exception = ex2;
                }
                IAsyncDisposable tmp = _test as IAsyncDisposable;
                if (tmp != null)
                {
                    awaiter = tmp.DisposeAsync().GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        _state = num = 0;
                        _awaiter = awaiter;
                        var stateMachine = this;
                        _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                        return;
                    }
                }
                else
                {
                    _test.Dispose();
                    syncComplete = true;
                }
                       
            }
            else
            {
                awaiter = _awaiter;
                _awaiter = new TaskAwaiter();
                _state = num = -1;
            }

            if (!syncComplete)
            {
                awaiter.GetResult();
                awaiter = new TaskAwaiter();
                ex2 = _exception;
                if (ex2 != null)
                {
                    exception = ex2 as Exception;
                    if (exception == null)
                    {
                        throw ex2;
                    }
                    ExceptionDispatchInfo.Capture(exception).Throw();
                }
                _exception = null;
            }
        }
        catch (Exception ex1)
        {
            exception = ex1;
            _state = -2;
            _builder.SetException(exception);
            return;
        }
        _state = -2;
        _builder.SetResult();
    }
Если вы внимательно посмотрите на логику, то сможете заметить, что для того чтобы старый код заработал, я добавил новую переменную syncComplete, которую выставляю в true только в том случае, если операцию выполнил в синхронном режиме.
Также приплюсовался блок кода, в котором добавилась проверка на имплементацию интерфейса IAsyncDisposable.
IAsyncDisposable tmp = _test as IAsyncDisposable;
if (tmp != null)
{
    awaiter = tmp.DisposeAsync().GetAwaiter();
    if (!awaiter.IsCompleted)
    {
        _state = num = 0;
        _awaiter = awaiter;
        var stateMachine = this;
        _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
        return;
    }
}
else
{
    _test.Dispose();
    syncComplete = true;
}
Так как на предыдущем шаге я убрал с класса Test наследование от данного интерфейса, вы можете запустить код и удостовериться, что он успешно работает. Если вернем в него наследование от интерфейса IAsyncDisposable и запустим наш проект, то реализация будет аналогичной примеру с использованием блока async выше.

Итоги
Пользу этой статьи для использования на практике сложно оценить. Я ставил задачу показать, что новые возможности языка, как реализация async disposable, не являются архисложными, с точки зрения компилятора. Этот компилятор сгенерировал код, я немного его дополнил, и дальше он сделал то, что я от него запрашивал. В целом, очень хотелось написать материал о том, что можно поломать немного мозг над еще нереализованными фичами и определить, насколько мудрено их будет реализовать.  Спасибо за то, что дочитали эту головоломку до конца. 

No comments:

Post a Comment