Как выполнить некоторый код, когда блок выходит из-за «ожидания»?

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: я хочу

  1. Журнал, когда управление выходит из DoWhatever() и возвращается к вызывающей стороне, будь то из-за ожидания или из-за "естественного" выхода из Делай что угодно().
  2. Сделайте это в том же потоке, который вызывал DoWhatever().
  3. Предпочтительно делать это с помощью предложения «using», показанного выше, потому что это шаблон уже используется во многих местах и ​​отлично работает без Ждите.

Я думаю, у вас есть некоторые ошибочные предположения о том, как работает await. А именно из этого высказывания - when an "await" causes a method call to return

Jonesopolis 10.01.2023 18:09

@Jonesopolis, но на самом деле это то, что моделирует await: сопрограммы, между которыми указатель инструкции прыгает вперед и назад, входя и выходя из каждой сопрограммы при каждом прыжке. Так что формулировка не удалась, на мой взгляд. Тот факт, что await реализован с помощью сгенерированного конечного автомата, является деталью реализации.

Good Night Nerd Pride 10.01.2023 18:14

@Jonesopolis Я почти уверен, что await действительно возвращает управление вызывающей стороне, если ожидаемый объект еще не завершен.

markltx 10.01.2023 18:20

@Evk Согласен, это странно, но у меня есть на то свои причины. Это потому, что регистратор поддерживает свой собственный стек вызовов для каждого потока. Async/await ломает структуру логгера, потому что позволяет методу начинаться и заканчиваться в разных потоках. Это также позволяет вызывающему методу выйти до вызываемого метода.

markltx 10.01.2023 19:28

Какой регистратор используете?

Fildor 10.01.2023 20:57

@Fildor Я автор регистратора под названием TracerX, который поддерживает регистрацию входа и выхода методов, и я пытаюсь заставить его работать с async/await. Предложение Evk использовать AsyncLocal, похоже, работает.

markltx 12.01.2023 23:20
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
91
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вы хотите регистрировать, когда Something завершает какие-либо синхронные действия и возвращает задачу, это легко сделать:

var task = Something(); 
/* log as you like */ 
await task;

Я полагаю, что не совсем понял, хочу ли я, чтобы ведение журнала выполнялось объектом, указанным в предложении «использование». Этот шаблон отлично работал до появления async/await, и ТОННА кода уже использует его.

markltx 10.01.2023 18:32

@Kluas Еще одна проблема с вашим предложением заключается в том, что ожидание может вернуть или не вернуть управление вызывающей стороне. Это зависит от того, завершено ли ожидаемое (Задание) или нет. Суть в том, что я хочу регистрировать, когда управление возвращается (или возвращается) к вызывающему. Это также должно быть выполнено тем же потоком, в котором началась DoWhatever().

markltx 10.01.2023 18:38

Нет, await в любом случае вернет управление вызывающей стороне. Если задача уже выполнена, то дополнительно завершается метод DoWhatever (и вызывается ваш Dispose).

Klaus Gütter 10.01.2023 18:47

«await в любом случае вернет управление вызывающей стороне». -- Это не правильно. У метода DoWhatever есть вторая часть, которая может включать больше await. Метод вернется либо при первом незавершенном ожидаемом await, либо при достижении конца метода.

Theodor Zoulias 10.01.2023 23:12
Ответ принят как подходящий

Удивительно, но это можно сделать, используя 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 произойдет, как обычно, в конце использования блока.

Другие вопросы по теме