在應用程式中,由于 AsyncLocal 的錯誤/意外值,我遇到了奇怪的行為:盡管我抑制了執行背景關系的流程,但 AsyncLocal.Value 屬性有時不會在新生成的任務的執行范圍內重置。
下面我創建了一個最小的可重現示例來演示該問題:
private static readonly AsyncLocal<object> AsyncLocal = new AsyncLocal<object>();
[TestMethod]
public void Test()
{
Trace.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
var mainTask = Task.Factory.StartNew(() =>
{
AsyncLocal.Value = "1";
Task anotherTask;
using (ExecutionContext.SuppressFlow())
{
anotherTask = Task.Run(() =>
{
Trace.WriteLine(AsyncLocal.Value); // "1" <- ???
Assert.IsNull(AsyncLocal.Value); // BOOM - FAILS
AsyncLocal.Value = "2";
});
}
Task.WaitAll(anotherTask);
});
mainTask.Wait(500000, CancellationToken.None);
}
在十分之九的運行中(在我的電腦上),測驗方法的結果是:
.NET 6.0.2
"1"
-> 測驗失敗
如您所見,測驗失敗是因為Task.Run在前一個值內執行的操作內仍存在于AsyncLocal.Value(訊息:)內1。
我的具體問題是:
- Why does this happen? I suspect this happens because Task.Run may use the current thread to execute the work load. In that case, I assume lack of async/await-operators does not force the creation of a new/separate ExecutionContext for the action. Like Stephen Cleary said "from the logical call context’s perspective, all synchronous invocations are “collapsed” - they’re actually part of the context of the closest async method further up the call stack". If that’s the case I do understand why the same context is used within the action.
Is this the correct explanation for this behavior? In addition, why does it work flawlessly sometimes (about 1 run out of 10 on my machine)?
- How can I fix this? Assuming that my theory above is true it should be enough to forcefully introduce a new async "layer", like below:
private static readonly AsyncLocal<object> AsyncLocal = new AsyncLocal<object>();
[TestMethod]
public void Test()
{
Trace.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
var mainTask = Task.Factory.StartNew(() =>
{
AsyncLocal.Value = "1";
Task anotherTask;
using (ExecutionContext.SuppressFlow())
{
var wrapper = () =>
{
Trace.WriteLine(AsyncLocal.Value);
Assert.IsNull(AsyncLocal.Value);
AsyncLocal.Value = "2";
return Task.CompletedTask;
};
anotherTask = Task.Run(async () => await wrapper());
}
Task.WaitAll(anotherTask);
});
mainTask.Wait(500000, CancellationToken.None);
}
This seems to fix the problem (it consistently works on my machine), but I want to be sure that this is a correct fix for this problem.
Many thanks in advance
uj5u.com熱心網友回復:
為什么會這樣?我懷疑發生這種情況是因為 Task.Run 可能使用當前執行緒來執行作業負載。
我懷疑它發生是因為Task.WaitAll將使用當前執行緒行內執行任務。
具體來說,Task.WaitAll呼叫Task.WaitAllCore,它將嘗試通過呼叫行內運行它Task.WrappedTryRunInline。我將假設自始至終都使用默認任務調度程式。在這種情況下,這將呼叫,如果委托已被TaskScheduler.TryRunInline呼叫,它將回傳false。因此,如果任務已經開始在執行緒池執行緒上運行,這將回傳到WaitAllCore,這將只是進行正常等待,并且您的代碼將按預期作業(10 分之 1)。
如果一個執行緒池執行緒還沒有拾起它(10 個中有 9 個),那么TaskScheduler.TryRunInline將呼叫TaskScheduler.TryExecuteTaskInline,其默認實作將呼叫Task.ExecuteEntryUnsafe,呼叫Task.ExecuteWithThreadLocal. Task.ExecuteWithThreadLocal具有應用ExecutionContextif one was capture的邏輯。假設沒有被捕獲,任務的委托只是被直接呼叫。
因此,似乎每一步都符合邏輯。從技術上講,ExecutionContext.SuppressFlow“不捕獲ExecutionContext”是什么意思,這就是正在發生的事情。這并不意味著“清除” ExecutionContext。有時任務在執行緒池執行緒上運行(沒有捕獲的ExecutionContext),并且WaitAll只會等待它完成。其他時候,任務將由WaitAll執行緒池執行緒而不是執行緒池執行緒行內執行,并且在這種情況下ExecutionContext不會清除(并且從技術上講也不會捕獲)。
您可以通過捕獲當前執行緒 IDwrapper并將其與執行Task.WaitAll. 我希望它們對于異步本地值(意外)被繼承的運行是同一個執行緒,并且對于異步本地值按預期作業的運行它們將是不同的執行緒。
uj5u.com熱心網友回復:
如果可以的話,我會首先考慮是否可以用單個共享快取替換特定于執行緒的快取。該應用程式可能早于有用的型別,例如ConcurrentDictionary.
如果無法使用單例快取,則可以使用一堆異步本地值。堆疊異步本地值是一種常見模式。我更喜歡將堆疊邏輯包裝成一個單獨的型別(AsyncLocalValue在下面的代碼中):
public sealed class AsyncLocalValue
{
private static readonly AsyncLocal<ImmutableStack<object>> _asyncLocal = new();
public object Value => _asyncLocal.Value?.Peek();
public IDisposable PushValue(object value)
{
var originalValue = _asyncLocal.Value;
var newValue = (originalValue ?? ImmutableStack<object>.Empty).Push(value);
_asyncLocal.Value = newValue;
return Disposable.Create(() => _asyncLocal.Value = originalValue);
}
}
private static AsyncLocalValue AsyncLocal = new();
[TestMethod]
public void Test()
{
Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
var mainTask = Task.Factory.StartNew(() =>
{
Task anotherTask;
using (AsyncLocal.PushValue("1"))
{
using (AsyncLocal.PushValue(null))
{
anotherTask = Task.Run(() =>
{
Console.WriteLine("Observed: " AsyncLocal.Value);
using (AsyncLocal.PushValue("2"))
{
}
});
}
}
Task.WaitAll(anotherTask);
});
mainTask.Wait(500000, CancellationToken.None);
}
此代碼示例使用Disposable.Create我的Nito.Disposables 庫。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/444559.html
標籤:c# asynchronous .net-core async-await task-parallel-library
下一篇:如何修復“(df[df['id']==id_value].name==1).any()”'系列的真值是模棱兩可的錯誤'?
