Как очистить/очистить объект AsyncLocal<T> в BackgroundService(Microsoft.Hosting.Extensions.BackgroundService)?

У меня есть фоновая служба, которая выполняет некоторые задачи и задания в долго работающем потоке. Для простоты мы будем рассматривать это как планировщик, который выполняет асинхронный вызов SQL и повторяет его. Теперь я использую AsyncLocal для хранения метаданных для целей регистрации в службе и хочу группировать каждое повторение задачи как транзакцию. Я полагал, что в отличие от веб-приложений BackgroundService будет иметь один объект AsyncLocal(ExecutionContext), который мне придется очищать после каждого повторения (каждого вызова SQL в этом примере). Что касается проблемы, я не могу очистить объект.

Вот реализация моего класса хранилища AsyncLocal.

 public class AsyncLocalStorage<T> : ILocalStorage<T>
    {
        private readonly AsyncLocal<T> _context;
        public AsyncLocalStorage()
        {
            _context = new AsyncLocal<T>(OnValueChanged);
        }

        private static void OnValueChanged(AsyncLocalValueChangedArgs<T> args)
        {
            Log("OnValueChanged! Prev: {0} ; Current: {1}", RuntimeHelpers.GetHashCode(args.PreviousValue), RuntimeHelpers.GetHashCode(args.CurrentValue));
        }

        public T GetData()
        {
            try
            {
                return _context.Value;
            }
            catch (Exception ex)
            {
                Log("Ex: " + ex.ToString());
            }

            return default(T);
        }

        public void SetData(T value)
        {
            _context.Value = value;
        }

        public void Clear()
        {
            _context.Value = default(T);
        }
    }

Здесь я присваиваю свои метаданные объекту AsyncLocal, а затем вызываю функцию Clear. Но экземпляр объекта остается. Я приложил журналы для дальнейшего использования.

02-05-2024 20:10:38 - [4:(DEBUG)<t:4>] - [TName: ]TRANSACTION STARTED (The AsyncLocal Object is created and I assign my object "Transaction" using context.SetData()) 
02-05-2024 20:10:38 - [1:(ERROR)<t:4>] - OnValueChanged! Prev: 0 ; Current: 5773521  
02-05-2024 20:10:39 - [4:(DEBUG)<t:4>] - [TName: ]The async SQL call is made.... (The Transaction object is modified context.GetData() . I guess the obj is passed to thread 9 )  
02-05-2024 20:10:39 - [1:(ERROR)<t:9>] - OnValueChanged! Prev: 0 ; Current: 5773521  
02-05-2024 20:10:39 - [4:(DEBUG)<t:4>] - [TName: ]---- random things are taken care off using the Transaction object that resulted in the following OnValueChanged logs .  
02-05-2024 20:10:39 - [1:(ERROR)<t:5>] - OnValueChanged! Prev: 0 ; Current: 5773521  
02-05-2024 20:10:39 - [1:(ERROR)<t:9>] - OnValueChanged! Prev: 5773521 ; Current: 0  
02-05-2024 20:10:39 - [1:(ERROR)<t:5>] - OnValueChanged! Prev: 5773521 ; Current: 0  
02-05-2024 20:10:39 - [1:(ERROR)<t:5>] - OnValueChanged! Prev: 0 ; Current: 5773521  
02-05-2024 20:10:39 - [1:(ERROR)<t:5>] - OnValueChanged! Prev: 5773521 ; Current: 0  
02-05-2024 20:10:39 - [4:(DEBUG)<t:4>] - [TName: ]The Async SQL call returned a Task object .  
02-05-2024 20:10:39 - [1:(ERROR)<t:9>] - OnValueChanged! Prev: 0 ; Current: 5773521  
02-05-2024 20:10:39 - [4:(DEBUG)<t:4>] - [TName: ]Processing of the metadata done on thread 4 is over.  
02-05-2024 20:10:39 - [1:(ERROR)<t:4>] - OnValueChanged! Prev: 5773521 ; Current: 0  
02-05-2024 20:10:39 - [4:(DEBUG)<t:9>] - [TName: ]Most of the processing on Thread 9 is also over and The Task returned by SQL call has finished. 
02-05-2024 20:10:39 - [4:(DEBUG)<t:9>] - [TName: ]Now the Transaction object is fetched with context.GetData() and is Loaded into a Thread Processor that runs independently using "ThreadPool.QueueWorkItem(Event, transaction) , which happens to be thread 10" 
02-05-2024 20:10:39 - [1:(ERROR)<t:10>] - OnValueChanged! Prev: 0 ; Current: 5773521  
02-05-2024 20:10:39 - [4:(DEBUG)<t:9>] - [TName: ]Everything seems to be over and its time to call context.Clear() //called. 
02-05-2024 20:10:39 - [1:(ERROR)<t:9>] - OnValueChanged! Prev: 5773521 ; Current: 0  
02-05-2024 20:10:39 - [4:(DEBUG)<t:9>] - [TName: ]Tried getting the transaction object here to verify using context.GetData(), returned null here.  
02-05-2024 20:10:39 - [4:(DEBUG)<t:10>] - [TName: ProcessThread]Working with the transaction object reference in the Event thread.  
02-05-2024 20:10:39 - [4:(DEBUG)<t:10>] - [TName: ProcessThread]Also working with the transaction object that is passed to this thread processor. Have no idea why the below log has occured.  
02-05-2024 20:10:39 - [1:(ERROR)<t:10>] - OnValueChanged! Prev: 5773521 ; Current: 0  (I am sure the transaction object goes out of scope here)
02-05-2024 20:10:39 - [4:(DEBUG)<t:4>] - [TName: ]TRANSACTION STARTED (new repetition, calling the GetData function again before creating a new transaction object and assigning. I only assign a new object if it is null) 
02-05-2024 20:10:40 - [1:(ERROR)<t:4>] - OnValueChanged! Prev: 0 ; Current: 5773521  
02-05-2024 20:10:40 - [4:(DEBUG)<t:4>] - [TName: ] Suprise suprise!! it is not NULL. I have no idea why it isn't .   
02-05-2024 20:10:40 - [4:(DEBUG)<t:4>] - [TName: ]I have already cleared all the component objects in the transaction object . A New transaction object is needed here. 

Почему это происходит? Есть ли что-то, чего мне не хватает?

Обновлено: Минимальный воспроизводимый пример приведен ниже. Я попытался сделать его маленьким

public class Worker : BackgroundService
 {
     private readonly ILogger<Worker> _logger;
     private readonly BackgroundTask _backgroundtask;

     public Worker(ILogger<Worker> logger, BackgroundTask task)
     {
         _logger = logger;
         _backgroundtask = task;
     }

     protected override async Task ExecuteAsync(CancellationToken stoppingToken)
     {
         while (!stoppingToken.IsCancellationRequested)
         {
             _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
             _backgroundtask.MakeTask();
             await Task.Delay(1000, stoppingToken);
         }
     }
 }

 public sealed class BackgroundTask
 {
     private readonly ILogger<BackgroundTask> _logger;
     
     public BackgroundTask(ILogger<BackgroundTask> logger)
     {
         _logger = logger;
     }

     public void MakeTask()
     {
         Transactions trans = TransactionService.GetCurrentTransaction();

         if (trans == null)
         {
             Console.WriteLine("[T:<{0}>] There is no TransactionObject", Thread.CurrentThread.ManagedThreadId);
             trans = TransactionService.GetOrCreateTransaction();
         }
         else
         {
             Console.WriteLine("[T:<{0}>] There is a Transaction Object", Thread.CurrentThread.ManagedThreadId);
         }

         trans.PrintTransactionName();
  
         Task.Run(() =>
         {
             trans.SetTransactionName("transaction2");
             trans.PrintTransactionName();

             trans.ClearEverything();
         });
     }
 }

 public class TransactionService
 {
     private static AsyncLocalStorage<Transactions> transactionContext;

     static TransactionService()
     {
         transactionContext = new AsyncLocalStorage<Transactions>();
     }

     public static Transactions GetCurrentTransaction()
     {
         return transactionContext.GetData();
     }

     public static Transactions GetOrCreateTransaction()
     {
         var transaction = GetCurrentTransaction();
         if (transaction == null)
         {
             transaction = new Transactions("Transaction1");
             transactionContext.SetData(transaction);
         }
         return transaction;
     }

     public static void RemoveOutstandingTransactions()
     {
         transactionContext.Clear();
     }
 }

 public class Transactions
 {
     private string transactionName;
     public Transactions(string trans)
     {
         transactionName = trans;
     }

     public void SetTransactionName(string trans)
     {
         transactionName = trans;
     }

     public void PrintTransactionName()
     {
         if (!string.IsNullOrEmpty(transactionName))
         {
             Console.WriteLine("[T:<{0}>] The transactionName is {1}", Thread.CurrentThread.ManagedThreadId, transactionName);
         }
     }

     public void ClearEverything()
     {
         transactionName = null;
         Console.WriteLine("[T:<{0}>] The Transaction Object is being cleared", Thread.CurrentThread.ManagedThreadId);
         TransactionService.RemoveOutstandingTransactions();

// Just checking if the object is cleared in this thread.

         if (TransactionService.GetCurrentTransaction() == null)
         {
             Console.WriteLine("[T:<{0}>] The Transaction Object is null after clearing", Thread.CurrentThread.ManagedThreadId);
         }
     }
 }

Создайте Worker-Service с ядром .NET, как показано в приведенном выше примере. Почему объект транзакции появляется в начале следующего повторения после очистки объекта AsyncLocal. Журналы будут выглядеть примерно так...

[T:<1>] There is no TransactionObject
[T:<1>] OnValueChanged! Prev: 0 ; Current: 72766
[T:<1>] The transactionName is Transaction1
[T:<9>] OnValueChanged! Prev: 0 ; Current: 72766
[T:<9>] The transactionName is transaction2
[T:<1>] OnValueChanged! Prev: 72766 ; Current: 0
[T:<9>] The Transaction Object is being cleared
[T:<9>] OnValueChanged! Prev: 72766 ; Current: 0
[T:<9>] The Transaction Object is null after clearing

[T:<6>] There is a Transaction Object
[T:<6>] OnValueChanged! Prev: 72766 ; Current: 0
[T:<9>] OnValueChanged! Prev: 0 ; Current: 72766
[T:<9>] The transactionName is transaction2
[T:<9>] The Transaction Object is being cleared
[T:<9>] OnValueChanged! Prev: 72766 ; Current: 0
[T:<9>] The Transaction Object is null after clearing
[T:<9>] OnValueChanged! Prev: 0 ; Current: 72766

И почему хеш объекта одинаковый?

Можете ли вы предоставить минимально воспроизводимый пример?

Guru Stron 02.05.2024 20:15

Я добавил минимальный воспроизводимый пример. Достаточно ли этого, чтобы объяснить мою загадку?

Morfhine 03.05.2024 15:04
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
2
129
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Почему это происходит? Есть ли что-то, что мне не хватает?

Да, AsyncLocal работает как своего рода стек «асинхронного потока» копирования при записи. В вашем случае (если упростить и сконцентрироваться только на интересных «асинхронных потоках») MakeTask помещает первое значение в AsyncLocal, то есть в основном в асинхронный поток №1. Когда вы запускаете Task.Run(), он создает новый асинхронный поток №2. Очистка в вашем случае происходит в asyncflow#2, что не влияет на asyncflow#1 из-за поведения копирования при записи, но ваш работник все еще находится в asyncflow#1, где ваш объект был первой созданной транзакцией. Из-за этого вы всегда видите один и тот же хеш. Чтобы добиться желаемого поведения, вы можете просто сделать метод MakeTask асинхронным, даже без ожиданий внутри, но ожидая его в своем while цикле; это приведет к созданию нового «асинхронного потока», и результат будет таким:

[T:<7>] There is no TransactionObject
OnValueChanged! Prev: 0 ; Current: 11429296
[T:<7>] The transactionName is Transaction1
[T:<7>] The transactionName is transaction2
[T:<7>] The Transaction Object is being cleared
OnValueChanged! Prev: 11429296 ; Current: 0
[T:<7>] The Transaction Object is null after clearing
[T:<7>] There is no TransactionObject
OnValueChanged! Prev: 0 ; Current: 41622463
[T:<7>] The transactionName is Transaction1
[T:<7>] The transactionName is transaction2
[T:<7>] The Transaction Object is being cleared
OnValueChanged! Prev: 41622463 ; Current: 0
[T:<7>] The Transaction Object is null after clearing
info: WorkerService1.Worker[0]
      Worker running at: 05/15/2024 22:59:18 +03:00
[T:<7>] There is no TransactionObject
info: WorkerService1.Worker[0]
      Worker running at: 05/15/2024 22:59:19 +03:00
OnValueChanged! Prev: 0 ; Current: 31364015
[T:<7>] The transactionName is Transaction1
[T:<7>] The transactionName is transaction2
[T:<7>] The Transaction Object is being cleared
OnValueChanged! Prev: 31364015 ; Current: 0
[T:<7>] The Transaction Object is null after clearing

Я понимаю . В качестве альтернативы мне пришлось бы вручную очищать AsyncLocal в каждом AsyncFlow, если я не хочу делать MakeTask() async.

Morfhine 16.05.2024 09:02

@Morfhine, Ну, это зависит от того, чего вы хотите достичь с помощью AsyncLocal,, потому что работа с AsyncLocal неасинхронными методами в некотором роде противоположна цели AsyncLocal, которая заключается в распространении данных через асинхронные потоки. Каждый асинхронный поток может изменять данные, но когда он возвращается к предыдущему потоку, изменения будут сброшены, а предыдущий асинхронный поток будет восстановлен, как в стеке. Вероятно, в вашем случае вы можете просто создать свой собственный класс области/контекста и распространять его вручную, например, IServiceScope.

Oleksandr Tolstikov 16.05.2024 09:13

@Morfhine, по сути, проблема вашего кода, на мой взгляд, заключается в том, что у вас есть логическое разделение, в котором MakeTask присваивает данные контексту, поэтому это отдельный логический элемент и он должен определять контекст. Однако, если вы работаете с AsyncLocal, вы уже находитесь в контексте, и если вы вернетесь к нему, то последние назначенные там данные будут восстановлены, но на самом деле этого не должно быть, потому что логически для вашего приложения вы находились в другом контексте, а не в другом. с точки зрения асинхронности.

Oleksandr Tolstikov 16.05.2024 09:16

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