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

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
Laravel с Turbo JS
Laravel с Turbo JS
Turbo - это библиотека JavaScript для упрощения создания быстрых и высокоинтерактивных веб-приложений. Она работает с помощью техники под названием...
Типы ввода HTML: Лучшие практики и советы
Типы ввода HTML: Лучшие практики и советы
HTML, или HyperText Markup Language , является стандартным языком разметки, используемым для создания веб-страниц. Типы ввода HTML - это различные...
Аутсорсинг разработки PHP для индивидуальных веб-решений
Аутсорсинг разработки PHP для индивидуальных веб-решений
Услуги PHP-разработки могут быть экономически эффективным решением для компаний, которые ищут высококачественные услуги веб-разработки по доступным...
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
Слишком много useState? Давайте useReducer!
Слишком много useState? Давайте useReducer!
Современный фронтенд похож на старую добрую веб-разработку, но с одной загвоздкой: страница в браузере так же сложна, как и бэкенд.
Узнайте, как использовать теги <ul> и <li> для создания неупорядоченных списков в HTML
Узнайте, как использовать теги <ul> и <li> для создания неупорядоченных списков в HTML
HTML предоставляет множество тегов для структурирования и организации содержимого веб-страницы. Одним из наиболее часто используемых тегов для...
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 произойдет, как обычно, в конце использования блока.

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