Здравствуйте, уважаемые читатели. Сегодняшняя статья тем отличается от остальных,
что имеет больше ознакомительный характер и отображает в большей мере мою субъективную точку зрения, чем обзор полезности для использования в своих проектах. Статья будет основываться на новых возможностях
языка 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.
На гитхабе в ветке о рослине ведутся две дискуссии по этому поводу. Первая дискуссия о 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
.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();
}
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