Сегодня мы рассмотрим тему async/await для C#
6, а именно – тот нюанс, что ключевое слово await теперь работает в блоках catch/finally. Не знаю, как вам, а мне очень
интересно, как же это все реализовано изнутри IL кода. Как Microsoft решил
ту или иную возможность в языке C#. Если вы
знаете, как работают таски с использованием ключевых слов async/await изнутри, и такие слова, как TaskAwaiter, не вызывают у вас недопонимания, тогда добро
пожаловать в углубленный анализ. Если же вам неизвестно, как кухня async и await работает изнутри, то вам, наверное, следует почитать
об этом, например, здесь: "Тонкости использования async и await".
Давайте рассмотрим пример с использованием
приведенных выше ключевых слов для C#
5.
class Program
{
static void
Main(string[] args)
{
TestAsyncMethod();
Console.ReadLine();
}
public static async void TestAsyncMethod()
{
try
{
await Task.FromResult(5);
}
catch
(Exception ex)
{
Console.WriteLine("Catch
Block");
}
finally
{
Console.WriteLine("Finally
Block Block");
}
}
}
Приведенный выше код не позволяет обрабатывать
асинхронно код в блоке catch или finally. Например, следующие изменения не
скомпилируются.
Все дело в том, что язык C# 5.0 не поддерживает такой синтаксис. Теперь
немного теории о том, что происходит внутри IL кода, если данный код скомпилировать.
У нас будет сгенерирован класс <TestAsyncMethod> d_0
В этом классе строится машина состояний, благодаря которой наш код работает
с ключевым словом await. К сожалению, IL Spy генерирует такой
код, который сложно читать, но если посмотреть на этот код с помощью .NET Reflector, сразу все станет на свои места.
private struct
<TestAsyncMethod>d__0 : IAsyncStateMachine
{
// Fields
public int
<>1__state;
private object <>t__stack;
private TaskAwaiter<int>
<>u__$awaiter1;
// Methods
private void
MoveNext()
{
{
bool flag = true;
switch (this.<>1__state)
{
case -3:
goto Label_00F4;
}
try
{
int num = this.<>1__state;
if (num == 0)
{
}
try
{
TaskAwaiter<int> awaiter;
num = this.<>1__state;
if (num != 0)
{
if (!awaiter.IsCompleted)
{
this.<>1__state = 0;
this.<>u__$awaiter1 = awaiter;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<TestAsyncMethod>d__0>(ref awaiter, ref this);
flag = false;
return;
}
}
else
{
awaiter = this.<>u__$awaiter1;
this.<>u__$awaiter1 = new TaskAwaiter<int>();
this.<>1__state = -1;
}
awaiter.GetResult();
awaiter = new TaskAwaiter<int>();
}
catch (Exception)
{
Console.WriteLine("Catch Block");
}
}
finally
{
if (flag)
{
Console.WriteLine("Finally Block Block");
}
}
}
catch (Exception exception2)
{
this.<>1__state = -2;
this.<>t__builder.SetException(exception2);
return;
}
Label_00F4:
this.<>1__state = -2;
this.<>t__builder.SetResult();
}
[DebuggerHidden]
private void
SetStateMachine(IAsyncStateMachine param0)
{
this.<>t__builder.SetStateMachine(param0);
}
}
Как видите, код нестрашный и вполне читабельный. Если вы
внимательно на него посмотрите, то увидите, что компилятор сгенерировал обычный
switch и переход
между состояниями машины с помощью goto. Если переписать выше код C#, который лучше читаемый для человеческого глаза, то мы увидим, что все не
так уж страшно выглядит.
Переписанный пример в читабельный вид:
public static void
TestAsyncMethod()
{
var builder = new AsyncVoidMethodBuilder();
int state = 0;
Action moveNext = null;
TaskAwaiter<int>
awaiter = new TaskAwaiter<int>();
moveNext = () =>
{
try
{
bool
flag = true;
switch (state)
{
case
-3:
goto
Label_00F4;
}
try
{
int
num = state;
if
(num == 0)
{
}
try
{
TaskAwaiter<int> aw;
num = state;
if
(num != 0)
{
aw = Task.FromResult<int>(5).GetAwaiter();
if
(!awaiter.IsCompleted)
{
state = 0;
awaiter = aw;
awaiter.OnCompleted(moveNext);
flag = false;
return;
}
}
else
{
aw = awaiter;
awaiter = new TaskAwaiter<int>();
state = -1;
}
aw.GetResult();
aw = new TaskAwaiter<int>();
}
catch
(Exception)
{
Console.WriteLine("Catch
Block");
}
}
finally
{
if
(flag)
{
Console.WriteLine("Finally
Block Block");
}
}
}
catch
(Exception exception2)
{
state = -2;
builder.SetException(exception2);
return;
}
Label_00F4:
state = -2;
builder.SetResult();
};
moveNext();
}
А теперь посмотрим пример на C 6.0 и разберем, что изменилось в новой версии языка.
А теперь посмотрим пример на C 6.0 и разберем, что изменилось в новой версии языка.
class Program
{
static void
Main(string[] args)
{
TestCatchMachodWithAsync();
ReadLine();
}
private static async void
TestCatchMachodWithAsync()
{
try
{
WriteLine(await Task.Factory.StartNew(() =>
{
int
res = 0;
for(int
i = 0; i < 100; i++)
{
res += i;
if(i
== 20)
throw new Exception("Hello
World");
}
return res;
}));
}
catch
(Exception ex)
{
WriteLine(await Task.FromResult("Catch
block"));
}
finally
{
WriteLine(await Task.FromResult("Finally
block"));
}
}
}
Этот пример работает в C# 6.0. Давайте
посмотрим, какой IL код был
сгенерирован. Для этого скомпилируем проект в Visual Studio 2015 Preview, а затем откроем наш экзешник в .Net Reflector.
[CompilerGenerated]
private sealed class
<TestCatchMachodWithAsync>d__1 : IAsyncStateMachine
{
// Fields
public int
<>1__state;
public object <>7__wrap1;
public int
<>7__wrap2;
public object <>7__wrap3;
public int
<>7__wrap4;
public
AsyncVoidMethodBuilder <>t__builder;
public TaskAwaiter<int>
<>u__$awaiter3;
public TaskAwaiter<string> <>u__$awaiter4;
// Methods
public <TestCatchMachodWithAsync>d__1();
private void
MoveNext();
[DebuggerHidden]
private void
SetStateMachine(IAsyncStateMachine stateMachine);
}
Теперь раскроем полностью весь код и посмотрим, что изменилось.
[CompilerGenerated]
private sealed class
<TestCatchMachodWithAsync>d__1 : IAsyncStateMachine
{
// Fields
public int
<>1__state;
public object <>7__wrap1;
public int
<>7__wrap2;
public object <>7__wrap3;
public int
<>7__wrap4;
public
AsyncVoidMethodBuilder <>t__builder;
public TaskAwaiter<int>
<>u__$awaiter3;
public TaskAwaiter<string> <>u__$awaiter4;
// Methods
private void
MoveNext()
{
Exception exception3;
int
num = this.<>1__state;
try
{
Program.<TestCatchMachodWithAsync>d__1 d__;
TaskAwaiter<string> awaiter2;
string str2;
object obj2;
switch (num)
{
case
0:
case
1:
break;
case
2:
goto
Label_01F6;
default:
this.<>7__wrap1 = null;
this.<>7__wrap2 = 0;
break;
}
try
{
int
result;
switch (num)
{
case
0:
break;
case
1:
goto Label_0160;
default:
this.<>7__wrap4 = 0;
break;
}
try
{
TaskAwaiter<int>
awaiter;
if
(num != 0)
{
awaiter =
Task.Factory.StartNew<int>(Program.<>c__DisplayClass0.CS$<>9__CachedAnonymousMethodDelegate2 ?? (Program.<>c__DisplayClass0.CS$<>9__CachedAnonymousMethodDelegate2 = new Func<int>(Program.<>c__DisplayClass0.CS$<>9__inst.<TestCatchMachodWithAsync>b__1))).GetAwaiter();
if
(!awaiter.IsCompleted)
{
this.<>1__state = num = 0;
this.<>u__$awaiter3 = awaiter;
d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<TestCatchMachodWithAsync>d__1>(ref awaiter, ref d__);
return;
}
}
else
{
awaiter = this.<>u__$awaiter3;
this.<>u__$awaiter3 = new TaskAwaiter<int>();
this.<>1__state = num = -1;
}
result =
awaiter.GetResult();
awaiter = new
TaskAwaiter<int>();
int
num2 = result;
Console.WriteLine(num2);
}
catch
(Exception exception)
{
this.<>7__wrap3 = exception;
this.<>7__wrap4 = 1;
}
result = this.<>7__wrap4;
if
(result != 1)
{
goto Label_019D;
}
Exception exception2 =
(Exception) this.<>7__wrap3;
awaiter2 = Task.FromResult<string>("Catch
block").GetAwaiter();
if
(awaiter2.IsCompleted)
{
goto
Label_017D;
}
this.<>1__state = num = 1;
this.<>u__$awaiter4 = awaiter2;
d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<TestCatchMachodWithAsync>d__1>(ref awaiter2, ref d__);
return;
Label_0160:
awaiter2 = this.<>u__$awaiter4;
this.<>u__$awaiter4 = new TaskAwaiter<string>();
this.<>1__state = num = -1;
Label_017D:
str2 = awaiter2.GetResult();
awaiter2 = new
TaskAwaiter<string>();
string str = str2;
Console.WriteLine(str);
Label_019D:
this.<>7__wrap3 = null;
}
catch
(object obj1)
{
obj2 = obj1;
this.<>7__wrap1 = obj2;
}
awaiter2 = Task.FromResult<string>("Finally
block").GetAwaiter();
if
(awaiter2.IsCompleted)
{
goto
Label_0213;
}
this.<>1__state = num = 2;
this.<>u__$awaiter4 = awaiter2;
d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<TestCatchMachodWithAsync>d__1>(ref awaiter2, ref d__);
return;
Label_01F6:
awaiter2 = this.<>u__$awaiter4;
this.<>u__$awaiter4 = new TaskAwaiter<string>();
this.<>1__state = num = -1;
Label_0213:
str2 = awaiter2.GetResult();
awaiter2 = new
TaskAwaiter<string>();
string str3 = str2;
Console.WriteLine(str3);
obj2 = this.<>7__wrap1;
if
(obj2 != null)
{
exception3 = obj2 as
Exception;
if
(exception3 == null)
{
throw
obj2;
}
ExceptionDispatchInfo.Capture(exception3).Throw();
}
int
num1 = this.<>7__wrap2;
this.<>7__wrap1 = null;
}
catch
(Exception exception4)
{
exception3 = exception4;
this.<>1__state = -2;
this.<>t__builder.SetException(exception3);
return;
}
this.<>1__state = -2;
this.<>t__builder.SetResult();
}
[DebuggerHidden]
private void
SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}
Как видим из примера, у нас изменения коснулись в основном переключения контекста, и стало больше меток goto. Попробуем переписать этот пример так, чтобы его можно было скомпилировать. Сначала с небольшими модификациями, а
затем приведем его в полностью читабельный вид. Интересно, что для теста я
компилировал и запускал на C# 5.0.
internal class Program
{
private static void Main(string[] args)
{
TestNewAsync();
Console.ReadLine();
}
private static void TestNewAsync()
{
int
state = 0;
object wrap1= null;
int
wrap2 = 0;
object wrap3 = null;
int
wrap4 = 0;
var
builder = new AsyncVoidMethodBuilder();
var
awaiter3 = new TaskAwaiter<int>();
var
awaiter4 = new TaskAwaiter<string>();
Action moveNext = null;
moveNext = () =>
{
Exception exception3;
int
num = state;
try
{
TaskAwaiter<string> awaiter2;
string str2;
object obj2;
switch (num)
{
case
0:
case
1:
break;
case
2:
goto
Label_01F6;
default:
wrap1 = null;
wrap2 = 0;
break;
}
try
{
int
result;
switch (num)
{
case
0:
break;
case
1:
goto
Label_0160;
default:
wrap4 = 0;
break;
}
try
{
TaskAwaiter<int> awaiter;
if
(num != 0)
{
awaiter = Task.Factory.StartNew<int>(GetResult).GetAwaiter();
if
(!awaiter.IsCompleted)
{
state = num =
0;
awaiter3 =
awaiter;
awaiter3.UnsafeOnCompleted(moveNext);
return;
}
}
else
{
awaiter = awaiter3;
awaiter3 = new TaskAwaiter<int>();
state = num = -1;
}
result =
awaiter.GetResult();
awaiter = new TaskAwaiter<int>();
int
num2 = result;
Console.WriteLine(num2);
}
catch
(Exception exception)
{
wrap3 = exception;
wrap4 = 1;
}
result = wrap4;
if
(result != 1)
{
goto
Label_019D;
}
Exception exception2 = (Exception) wrap3;
awaiter2 = Task.FromResult<string>("Catch block").GetAwaiter();
if
(awaiter2.IsCompleted)
{
goto
Label_017D;
}
state = num = 1;
awaiter4 = awaiter2;
awaiter4.UnsafeOnCompleted(moveNext);
return;
Label_0160:
awaiter2 = awaiter4;
awaiter4 = new TaskAwaiter<string>();
state = num = -1;
Label_017D:
str2 =
awaiter2.GetResult();
awaiter2 = new TaskAwaiter<string>();
string str = str2;
Console.WriteLine(str);
Label_019D:
wrap3 = null;
}
catch
(Exception obj1)
{
obj2 = obj1;
wrap1 = obj2;
}
awaiter2 = Task.FromResult<string>("Finally block").GetAwaiter();
if
(awaiter2.IsCompleted)
{
goto
Label_0213;
}
state = num = 2;
awaiter4 = awaiter2;
awaiter4.UnsafeOnCompleted(moveNext);
return;
Label_01F6:
awaiter2 = awaiter4;
awaiter4 = new TaskAwaiter<string>();
state = num = -1;
Label_0213:
str2 = awaiter2.GetResult();
awaiter2 = new TaskAwaiter<string>();
string str3 = str2;
Console.WriteLine(str3);
obj2 = wrap1;
if
(obj2 != null)
{
exception3 = obj2 as Exception;
if
(exception3 == null)
{
throw new Exception();
}
ExceptionDispatchInfo.Capture(exception3).Throw();
}
int
num1 = wrap2;
wrap1 = null;
}
catch
(Exception exception4)
{
exception3 = exception4;
state = -2;
builder.SetException(exception3);
return;
}
state = -2;
builder.SetResult();
};
moveNext();
}
public static int GetResult()
{
int
res = 0;
for(int
i = 0; i < 100; i++)
{
res += i;
if(i
== 20)
throw new Exception("Hello
World");
}
return res;
}
}
Читать такой код по-прежнему сложно, потому что продумать нормальные
имена, вместо генерируемых компилятором, не так уж просто. Но сейчас вы можете скопировать этот код и
запустить в вашей студии.
В данном коде можно увидеть пример использования класса ExceptionDispatchInfo c 4.5 фреймворка, который представляет исключение,
состояние которого захватывается в некоторой точке кода ссылка. Давайте разберем то, что здесь
происходит. Для того чтобы определить, какой сейчас блок должен выполняться, используется два блока switch. Первый блок:
switch (num)
{
case 0:
case 1:
break;
case 2:
goto
Label_01F6;
default:
wrap1 = null;
wrap2 = 0;
break;
}
Если у нас state == 2, значит, мы
переходим в блок finally. Второй switch используется для перехода в блок catch.
switch (num)
{
case 0:
break;
case 1:
goto
Label_0160;
default:
wrap4 = 0;
break;
}
Вот как это выглядит в реальном коде:
switch (num)
{
case 0:
case 1:
break;
case 2:
goto
Label_01F6;
default:
wrap1 = null;
wrap2 = 0;
break;
}
try
{
int result;
switch (num)
{
case
0:
break;
case
1:
goto
Label_0160;
default:
wrap4 = 0;
break;
}
Я долго думал, как можно переделать такой подход с оператором goto, на подход без goto, и мне пришла только идея с установкой какого-то
состояния и постоянно вызывать циклически самого себя. Схематически это
показано на примере ниже.
private static void TestAsync()
{
int state = 0;
Action moveNext = null;
moveNext = () =>
{
int
num = state;
switch (num)
{
case
1: //Main Block
{
Console.WriteLine("Main
block");
state = 3;
moveNext();
break;
}
case
3: // Catch block
{
state = 4;
Console.WriteLine("Catch
Block");
moveNext();
break;
}
case
4: // Finally block
{
Console.WriteLine("Finally
block");
break;
}
default:
{
state = 1;
moveNext();
break;
}
}
};
moveNext();
}
Но к сожалению, если вы перепишете код так, то он не станет читабельнее.
Я переписал данный код через один switch. Мне пришлось добавить свои состояния для
перехода. Если вы хотите писать такой код, то вам нужно написать свою
машину состояний. Это есть ничто иное, как паттерн State. Посмотрите на этот пример, он напоминает чем-то своим подходом то, что мы
реализуем сейчас. То есть, если хорошо продумать такой переход состояний, то
можно в коде обойтись без ключевого слова await. Последнее предложение было шуткой. Мне бы лично
не хотелось писать постоянно такой код. Мало того, что его сложно писать, так
кроме того, его не так-то просто понять и сопровождать. А так как мы привыкли, что всю черновую работу за нас делает компилятор, то проделывать эту работу самим за него не очень хорошо. Продолжим наш разбор
кода.
Начнем, пожалуй, с переменных, которые мы объявили.
int state = 0;
object wrap1= null;
int wrap2 = 0;
object wrap3 = null;
int wrap4 = 0;
var builder = new AsyncVoidMethodBuilder();
var awaiter3 = new TaskAwaiter<int>();
var awaiter4 = new TaskAwaiter<string>();
Action
moveNext = null;
Как мы уж разобрали, что переменная state – это хранение состояния машины. Следующие
переменные wrap1 и wrap3 используются для хранения ошибок на разных этапах
выполнения функции, а переменные wrap2/wrap4 используются для того, чтобы идентифицировать, успешно мы получили результат выполнения или нет. Это что-то вроде
вспомогательных состояний для определения успешности получения результата с
помощью функции GetResult класса TaskAwaiter. Давайте посмотрим кусок кода, чтобы понять, как
используются эти переменные.
try
{
TaskAwaiter<int>
awaiter;
if (num != 0)
{
awaiter = Task.Factory.StartNew<int>(GetResult).GetAwaiter();
if
(!awaiter.IsCompleted)
{
state = num = 0;
awaiter3 = awaiter;
awaiter3.UnsafeOnCompleted(moveNext);
return;
}
}
else
{
awaiter = awaiter3;
awaiter3 = new TaskAwaiter<int>();
state = num = -1;
}
result = awaiter.GetResult();
awaiter = new TaskAwaiter<int>();
int num2 = result;
Console.WriteLine(num2);
}
catch (Exception
exception)
{
wrap3 = exception;
wrap4 = 1;
}
result
= wrap4;
if (result != 1)
{
goto Label_019D;
}
Exception exception2 = (Exception) wrap3;
awaiter2
= Task.FromResult<string>("Catch
block").GetAwaiter();
if (awaiter2.IsCompleted)
{
goto Label_017D;
}
У нас приведен кусок кода, в котором мы ожидаем завершения результата
основного блока кода (тот, который у нас внутри try). В данном примере стоит обратить внимание на блок
catch, в котором мы
сохраняем ошибку, а также ставим флаг wrap4 в 1.
catch (Exception
exception)
{
wrap3 = exception;
wrap4 = 1;
}
Это делается для того, чтобы на следующем ходу определить результат
выполнения предыдущей операции.
result
= wrap4;
if (result != 1)
{
goto Label_019D;
}
Exception exception2 = (Exception) wrap3;
awaiter2
= Task.FromResult<string>("Catch
block").GetAwaiter();
if (awaiter2.IsCompleted)
{
goto Label_017D;
}
По умолчанию у нас, как вы помните, wrap4 равен нулю – начальная инициализация, и он будет
равен 1 только в том случае, если у нас произошла ошибка. Если результат
не равен нулю, то мы просто обнуляем переменную wrap3, в которую сохраняем ошибку, и переходим в блок finally. Если же у нас произошла все-таки ошибка, то мы
получим ее в блоке catch и запустим ожидание
завершения awaiter2 для данного блока
catch. Интересно, что если
посмотреть на эту структуру со стороны, то можно увидеть, что данный код имеет
следующую структуру:
try // Try Base block
{
try // Try finally block
{
try // Try catch block
{
}
catch // Catch catch block
{
}
}
catch // Catch finally catch block
{
}
}
catch //catch Base block
{
}
Ну вот в такую структуру и развернут наш код, который сгенерировал нам
компилятор языка C# 6.0.
У нас есть признак того, успешно мы завершили блок try или нет (метка wrap 4). Если успешно, переходим сразу в конец блока catch (в приведенном схематическом примере выше это блок
с меткой “Catch catch block”), так как за ним
начнется выполнение автоматом блока finally.
}
catch (Exception obj1)
{
obj2 = obj1;
wrap1 = obj2;
}
awaiter2
= Task.FromResult<string>("Finally
block").GetAwaiter();
if (awaiter2.IsCompleted)
{
goto Label_0213;
}
state =
num = 2;
awaiter4
= awaiter2;
awaiter4.UnsafeOnCompleted(moveNext);
Этот кусок блока отвечает за блок finally в нашем исходном примере. Только в этом блоке,
когда мы закончили нашу обработку, мы проверяем, не произошло ли у нас каких-либо
ошибок.
str2 =
awaiter2.GetResult();
awaiter2
= new TaskAwaiter<string>();
string str3 = str2;
Console.WriteLine(str3);
obj2 =
wrap1;
if (obj2 != null)
{
exception3 = obj2 as Exception;
if (exception3 == null)
{
throw new Exception();
}
ExceptionDispatchInfo.Capture(exception3).Throw();
}
int num1 = wrap2;
wrap1 = null;
Если такие ошибки есть, то мы прокидываем эти ошибки дальше. Мне кажется,
что .NET Reflector как-то неверно сгенерировал код для этой части, потому что вместо данного кода
if (obj2 != null)
{
exception3 = obj2 as Exception;
if (exception3 == null)
{
throw new Exception();
}
ExceptionDispatchInfo.Capture(exception3).Throw();
}
Изначальный код был вот такой:
if (obj2 != null)
{
exception3 = obj2 as Exception;
if (exception3 == null)
{
throw
obj2;
}
ExceptionDispatchInfo.Capture(exception3).Throw();
}
Если нам модифицировать наш код с использованием вместо object типа Exception, тогда все стает на свои места. Так что если вы захотите все-таки привести этот код
к читаемому виду и эмуляцией работы try/catch/finally блока с ключевым словом await, то вас ждет рутинная, но, как мне кажется, интересная работа (если вам интересно постоянно докапываться до чего-то нового). Если
мы вместо void будем
возвращать Task, то ваш код вырастет почти в два раза, так как с
таском немного сложнее в том плане, что нам уже нужно устанавливать конкретный
результат, который для нашего AsyncVoidMethodBuilder был пустым.
state = -2;
builder.SetResult();
А так нам может понадобиться установить результат на одном из этапов
выполнения нашего кода.
На этом буду заканчивать статью по async/await в C# 6.0. Как видим, особых изменений не случилось,
только расширилось построение IL кода, для того чтобы
генерировать актуальную машину состояний. Следующую статью по C# 6.0 я посвящу фильтрам исключений. Уже есть
замечательная статья по данной теме от Сергея Теплякова "Фильтры исключений в C# 6.0", ну а я постараюсь
осветить свое виденье данной темы и покопаюсь немного в IL коде, разбирая подноготную фильтров исключений в 6-м шарпе. Надеюсь, это будет не очень скучно, потому что я постараюсь высветлить фильтры ошибок с более низкого уровня, чем
это описано у Теплякова. Получится у меня или нет – как говорят, поживем-увидим.
No comments:
Post a Comment