У меня есть фоновая служба, которая выполняет некоторые задачи и задания в долго работающем потоке. Для простоты мы будем рассматривать это как планировщик, который выполняет асинхронный вызов 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
И почему хеш объекта одинаковый?
Я добавил минимальный воспроизводимый пример. Достаточно ли этого, чтобы объяснить мою загадку?





Почему это происходит? Есть ли что-то, что мне не хватает?
Да, 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, Ну, это зависит от того, чего вы хотите достичь с помощью AsyncLocal,, потому что работа с AsyncLocal неасинхронными методами в некотором роде противоположна цели AsyncLocal, которая заключается в распространении данных через асинхронные потоки. Каждый асинхронный поток может изменять данные, но когда он возвращается к предыдущему потоку, изменения будут сброшены, а предыдущий асинхронный поток будет восстановлен, как в стеке. Вероятно, в вашем случае вы можете просто создать свой собственный класс области/контекста и распространять его вручную, например, IServiceScope.
@Morfhine, по сути, проблема вашего кода, на мой взгляд, заключается в том, что у вас есть логическое разделение, в котором MakeTask присваивает данные контексту, поэтому это отдельный логический элемент и он должен определять контекст. Однако, если вы работаете с AsyncLocal, вы уже находитесь в контексте, и если вы вернетесь к нему, то последние назначенные там данные будут восстановлены, но на самом деле этого не должно быть, потому что логически для вашего приложения вы находились в другом контексте, а не в другом. с точки зрения асинхронности.
Можете ли вы предоставить минимально воспроизводимый пример?