tldr: есть ли способ выполнить некоторый код, когда «ожидание» вызывает возврат вызова метода?
Предположим, я регистрирую вход и выход методов C# с объектом, чей метод Dispose() регистрирует выход метода. Например
void DoWhatever()
{
using (LogMethodCall("DoWhatever")
{
// do whatever
}
}
То есть метод LogMethodCall() записывает в журнал «DoWhatever enter», а затем возвращает объект типа CallEnder, чей метод Dispose() регистрирует «DoWhatever exiting». Это работает нормально, пока не используется await. Например...
async Task DoWhatever()
{
using (LogMethodCall("DoWhatever")
{
// do first part.
await Something();
// do second part.
}
}
Вышеприведенный код возвращает задачу вызывающей стороне, когда она попадает в ожидание, а остальная часть кода (включая вызов CallEnder.Dispose()) выполняется в этой задаче. Моя проблема в том, что я хочу регистрировать «Выход DoWhatever», когда ожидание запускает фактический возврат, а не когда наконец вызывается CallEnder.Dispose().
Есть ли способ сделать это? Есть ли что-то вроде события, которое возникает, когда ожидание вызывает возврат DoWhatever()? Может быть, что-то делать с ExecutionContext или CallContext или TaskScheduler?
Обратите внимание, что мне нужно сохранить шаблон «использование (some_object)», описанный в приведенном выше коде. Этот шаблон хорошо работает для регистрации входа и выхода из блока. Я могу изменить реализацию some_object, чтобы определить, когда управление возвращается из DoWhatever() в вызывающую программу, но я бы предпочел не менять реализацию DoWhatever(). Хотя мог бы, если бы не было другого выхода.
Дальнейшее уточнение ETA: я хочу
@Jonesopolis, но на самом деле это то, что моделирует await
: сопрограммы, между которыми указатель инструкции прыгает вперед и назад, входя и выходя из каждой сопрограммы при каждом прыжке. Так что формулировка не удалась, на мой взгляд. Тот факт, что await
реализован с помощью сгенерированного конечного автомата, является деталью реализации.
@Jonesopolis Я почти уверен, что await действительно возвращает управление вызывающей стороне, если ожидаемый объект еще не завершен.
@Evk Согласен, это странно, но у меня есть на то свои причины. Это потому, что регистратор поддерживает свой собственный стек вызовов для каждого потока. Async/await ломает структуру логгера, потому что позволяет методу начинаться и заканчиваться в разных потоках. Это также позволяет вызывающему методу выйти до вызываемого метода.
Какой регистратор используете?
@Fildor Я автор регистратора под названием TracerX, который поддерживает регистрацию входа и выхода методов, и я пытаюсь заставить его работать с async/await. Предложение Evk использовать AsyncLocal, похоже, работает.
Если вы хотите регистрировать, когда Something
завершает какие-либо синхронные действия и возвращает задачу, это легко сделать:
var task = Something();
/* log as you like */
await task;
Я полагаю, что не совсем понял, хочу ли я, чтобы ведение журнала выполнялось объектом, указанным в предложении «использование». Этот шаблон отлично работал до появления async/await, и ТОННА кода уже использует его.
@Kluas Еще одна проблема с вашим предложением заключается в том, что ожидание может вернуть или не вернуть управление вызывающей стороне. Это зависит от того, завершено ли ожидаемое (Задание) или нет. Суть в том, что я хочу регистрировать, когда управление возвращается (или возвращается) к вызывающему. Это также должно быть выполнено тем же потоком, в котором началась DoWhatever().
Нет, await
в любом случае вернет управление вызывающей стороне. Если задача уже выполнена, то дополнительно завершается метод DoWhatever
(и вызывается ваш Dispose).
«await
в любом случае вернет управление вызывающей стороне». -- Это не правильно. У метода DoWhatever
есть вторая часть, которая может включать больше await
. Метод вернется либо при первом незавершенном ожидаемом await
, либо при достижении конца метода.
Удивительно, но это можно сделать, используя AsyncLocal. AsyncLocal
похож на ThreadLocal
, за исключением того, что он проходит через асинхронный код, который может переключать потоки. У него есть конструктор, который позволяет вам прослушивать изменения значений и даже сообщает вам, почему значение изменилось. Его можно изменить либо потому, что вы явно установили Value
, либо если происходит асинхронное переключение контекста (в этом случае Value
изменяется на ноль/по умолчанию, когда управление уходит, и возвращается к исходному значению, когда управление возвращается). Это позволяет нам определить, когда будет достигнуто первое ожидание, и не только первое ожидание, но и ожидание, которое введет переключение контекста (например, await Task.CompletedTask
не вызовет переключение контекста). Таким образом, при первом таком переключении Task
будет возвращен вызывающему абоненту.
Вот пример кода:
public class Program {
public static void Main() {
var task = Test();
Console.WriteLine("Control flow got out of Test");
task.Wait();
}
static async Task Test() {
using (LogMethodCall()) {
await Task.Delay(1000);
Console.WriteLine("Finished using block");
}
}
static IDisposable LogMethodCall([CallerMemberName] string methodName = null) {
return new Logger(methodName);
}
private class Logger : IDisposable {
private readonly string _methodName;
private AsyncLocal<object> _alocal;
private bool _disposed;
public Logger(string methodName) {
Console.WriteLine($"{methodName} entered");
_methodName = methodName;
_alocal = new AsyncLocal<object>(OnChanged);
_alocal.Value = new object();
}
private void OnChanged(AsyncLocalValueChangedArgs<object> args) {
if (_disposed)
return;
// this property tells us that value changed because of context switch
if (args.ThreadContextChanged) {
Dispose();
}
}
public void Dispose() {
// prevent multiple disposal
if (_disposed)
return;
_disposed = true;
_alocal = null;
Console.WriteLine($"{_methodName} exited");
}
}
}
Он выводит:
Test entered
Test exited
Control flow got out of Test
Finished using block
Вы можете использовать тот же код и для обычных функций, потому что в них асинхронный локальный никогда не изменится, и поэтому dispose произойдет, как обычно, в конце использования блока.
Я думаю, у вас есть некоторые ошибочные предположения о том, как работает
await
. А именно из этого высказывания -when an "await" causes a method call to return