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