AsyncLocal意外地恢复其值-不涉及async/await



这是一个复杂的情况,请耐心等待。

我有一个BagOfProperties类,我想为它创建一个实例AsyncLocal,像这样

private readonly static System.Threading.AsyncLocal<BagOfProperties> _bag = new();
private static BagOfProperties Bag => _bag.Value ??= new BagOfProperties();

在代码中的任何地方,我现在通过Bag变量访问属性,很好地使它们都成为AsyncLocal。

现在,由于原因,我希望能够重置这些属性。容易实现:

private static void ResetBag()
{
_bag.Value = null;
}

由于_bagAsyncLocal,这在任何时候都是安全的,使用包的现有任务仍然可以访问旧值,但新任务将首先调用构造函数。

下一步是,我想从WinForms的Application.Idle事件处理程序调用ResetBag。这是它中断的地方(在某些竞速条件下-取决于我放置断点的位置,它可能工作)。

发生的是,_bag.Value被设置为null,但然后我们继续堆栈直到System.Threading.ExecutionContext.RunInternal,System.Threading.ExecutionContextSwitcher.Undo()被调用,_bag.Value被恢复到以前的版本。

在整个堆栈上甚至没有一个async调用!

我添加了以下ValueChangedHandler。

private static void ValueChangedHandler(System.Threading.AsyncLocalValueChangedArgs<BagOfProperties> asyncLocalValueChangedArgs)
{
if (System.Threading.Thread.CurrentThread.ManagedThreadId == 1)
{
var previousValue = asyncLocalValueChangedArgs.PreviousValue;
var currentValue = asyncLocalValueChangedArgs.CurrentValue;
var contextChanged = asyncLocalValueChangedArgs.ThreadContextChanged;
}
}

我只对ManagedThreadId == 1感兴趣,因为现在我的代码中没有异步,但事情发生在其他线程的第三方组件中。

当值被设置为null时,堆栈看起来像这样(星号是我的)。

MyHelper.dll!MyApp.Class1.ValueChangedHandler(System.Threading.AsyncLocalValueChangedArgs<MyApp.Class1.BagOfProperties> asyncLocalValueChangedArgs = {System.Threading.AsyncLocalValueChangedArgs<MyApp.Class1.BagOfProperties>}) Line 28    C#
mscorlib.dll!System.Threading.AsyncLocal<MyApp.Class1.BagOfProperties>.System.Threading.IAsyncLocal.OnValueChanged(object previousValueObj, object currentValueObj, bool contextChanged)    Unknown
mscorlib.dll!System.Threading.ExecutionContext.SetLocalValue(System.Threading.IAsyncLocal local, object newValue, bool needChangeNotifications)    Unknown
mscorlib.dll!System.Threading.AsyncLocal<System.__Canon>.Value.set(System.__Canon value)    Unknown
MyHelper.dll!MyApp.Class1.ResetBag() Line 21    C#
[Native to Managed Transition]    
[Managed to Native Transition]    
* MyFramework.Win.dll!MyFramework.MyUtils.Application_Idle(object sender = {System.Threading.Thread}, System.EventArgs e = {System.EventArgs}) Line 3872    C#
System.Windows.Forms.dll!System.Windows.Forms.Application.RaiseIdle(System.EventArgs e)    Unknown
MyFramework.Win.dll!MyFramework.MyUtils.TrackPopupMenu.AnonymousMethod__136_1() Line 3697    C#
[Native to Managed Transition]    
[Managed to Native Transition]    
mscorlib.dll!System.Delegate.DynamicInvokeImpl(object[] args)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbackDo(System.Windows.Forms.Control.ThreadMethodEntry tme)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbackHelper(object obj)    Unknown
* mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)    Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)    Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallback(System.Windows.Forms.Control.ThreadMethodEntry tme)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbacks()    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.WndProc(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.ScrollableControl.WndProc(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Form.WndProc(ref System.Windows.Forms.Message m)    Unknown
MyFramework.Win.dll!MyFramework.MyForm.WndProc(ref System.Windows.Forms.Message m = {System.Windows.Forms.Message}) Line 4309    C#
System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg = 49641, System.IntPtr wparam, System.IntPtr lparam)    Unknown
[Native to Managed Transition]    
[Managed to Native Transition]    
System.Windows.Forms.dll!System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(System.IntPtr dwComponentID, int reason = -1, int pvLoopData = 0)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(int reason = -1, System.Windows.Forms.ApplicationContext context = {System.Windows.Forms.ApplicationContext})    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoop(int reason, System.Windows.Forms.ApplicationContext context)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Application.Run(System.Windows.Forms.Form mainForm)    Unknown
MyApp.exe!MyApp.Program.Main() Line 25    C#

值恢复后的堆栈是这样的:

MyHelper.dll!MyApp.Class1.ValueChangedHandler(System.Threading.AsyncLocalValueChangedArgs<MyApp.Class1.BagOfProperties> asyncLocalValueChangedArgs = {System.Threading.AsyncLocalValueChangedArgs<MyApp.Class1.BagOfProperties>}) Line 28    C#
mscorlib.dll!System.Threading.AsyncLocal<MyApp.Class1.BagOfProperties>.System.Threading.IAsyncLocal.OnValueChanged(object previousValueObj, object currentValueObj, bool contextChanged)    Unknown
mscorlib.dll!System.Threading.ExecutionContext.OnAsyncLocalContextChanged(System.Threading.ExecutionContext previous = {System.Threading.ExecutionContext}, System.Threading.ExecutionContext current = {System.Threading.ExecutionContext})    Unknown
mscorlib.dll!System.Threading.ExecutionContextSwitcher.Undo()    Unknown
* mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)    Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)    Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallback(System.Windows.Forms.Control.ThreadMethodEntry tme)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbacks()    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.WndProc(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.ScrollableControl.WndProc(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Form.WndProc(ref System.Windows.Forms.Message m)    Unknown
MyFramework.Win.dll!MyFramework.MyForm.WndProc(ref System.Windows.Forms.Message m = {System.Windows.Forms.Message}) Line 4309    C#
System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg = 49641, System.IntPtr wparam, System.IntPtr lparam)    Unknown
[Native to Managed Transition]    
[Managed to Native Transition]    
System.Windows.Forms.dll!System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(System.IntPtr dwComponentID, int reason = -1, int pvLoopData = 0)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(int reason = -1, System.Windows.Forms.ApplicationContext context = {System.Windows.Forms.ApplicationContext})    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoop(int reason, System.Windows.Forms.ApplicationContext context)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Application.Run(System.Windows.Forms.Form mainForm)    Unknown
MyApp.exe!MyApp.Program.Main() Line 25    C#

是否有办法确保ResetBag将与正确的ExecutionContext一起运行?顺便说一句,AsyncLocal.Value的实现在这里。

应用程序是。net Framework 4.8, Windows Forms.

这似乎是你试图使用AsyncLocal<T>的方式,它不是设计的。异步本地值向下流动;调用堆栈永远不会"up"。理想情况下,它们是不可变的,这迫使代码设置Value,而不是依赖于副作用(这在async local中很难考虑)。

听起来你想要的更像是快照语义;我会查看System.Collections.Immutable(听起来像ImmutableDictionary可以很好地为您服务)。让每个任务锁定并复制当前值(必要时惰性创建),然后使用其本地副本;让您的空闲处理程序锁定并复制当前值并将其设置为null。我认为没有必要用AsyncLocal<T>来描述当前的问题。

也许您可以采用与dotnet团队使用HttpContextAccessor相同的方法,因此您可以在BagOfProperties周围创建包装器并将其引用保存在AsyncLocal而不是实际的BagOfProperties上。

private class BagOfPropertiesWrapper
{
public BagOfProperties BagOfProperties { get; set; } = new();
}
private static AsyncLocal<BagOfPropertiesWrapper> _bagWrapper = new();
private static BagOfProperties Bag()
{
_bagWrapper.Value ??= new BagOfPropertiesWrapper();
return _bagWrapper.Value.BagOfProperties;
}
private static void ResetBag()
{
if (_bagWrapper.Value is not null)
{
_bagWrapper.Value.BagOfProperties = null;
}
}

所以现在当你做ResetBag时,你将清除BagOfProperties,这是从BagOfPropertiesWrapper引用的,这是由AsyncLocal流动的。

我已经找到了ExecutionContext中不希望发生变化的原因,以及针对我的情况的解决方法。

原因在堆栈的这一部分。

MyHelper.dll!MyApp.Class1.ResetBag() Line 21    C#
[Native to Managed Transition]    
[Managed to Native Transition]    
MyFramework.Win.dll!MyFramework.MyUtils.Application_Idle(object sender = {System.Threading.Thread}, System.EventArgs e = {System.EventArgs}) Line 3872    C#
System.Windows.Forms.dll!System.Windows.Forms.Application.RaiseIdle(System.EventArgs e)    Unknown
MyFramework.Win.dll!MyFramework.MyUtils.TrackPopupMenu.AnonymousMethod__136_1() Line 3697    C#
[Native to Managed Transition]    
[Managed to Native Transition]    
mscorlib.dll!System.Delegate.DynamicInvokeImpl(object[] args)    Unknown
System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbackDo(System.Windows.Forms.Control.ThreadMethodEntry tme)    Unknown

看起来是这样的,因为,出于不相关的原因,我有以下代码:

aForm.BeginInvoke(new Action(() => Application.RaiseIdle(EventArgs.Empty)));

事实证明,Control.BeginInvoke具有更改ExecutionContext的副作用(在我的情况下是不希望的),使回调使用"main"的副本ExecutionContext而不是主要的东西。

我已将其更改为以下内容:

var flowControl = System.Threading.ExecutionContext.SuppressFlow();
f.BeginInvoke(new Action(() =>
{
flowControl.Undo();
Application.RaiseIdle(EventArgs.Empty);
}));

这解决了我的问题,但是如果在调用SuppressFlowUndo之间创建Task可能会引入一个问题:该任务将意外地操作ExecutionContext/AsyncLocals的主版本(即与我当前的问题相反)。

最新更新