我是否需要手动更改 AsyncLocal 变量的值以在逻辑请求结束时"Dispose"/"Release"它



我从 MSDN 文档中读到了AsyncLocal<T>,但有一点对我来说仍然不清楚。

我正在研究类似上下文绑定的缓存/记忆的东西,它的目的很简单,就是在逻辑请求中存储数据。这类似于旧的HttpContext.Current,其中数据存储在请求中,并将在请求结束时释放。但是,就我而言,我希望与环境无关,因此实现不受约束于例如 MVC、ASP.NET Core、WCF 等 ASP.NET 同时仍然能够存储和检索绑定到逻辑请求的数据,而无需在逻辑上不同的请求之间共享它。

为了根据问题简化我的代码,它看起来有点像这样:

class ContextualStorageAccessor
{
// ConcurrentDictionary since it's okay if some parallel operations are used per logical request 
private readonly AsyncLocal<ConcurrentDictionary<string, object>> _csm = new AsyncLocal<ConcurrentDictionary<string, object>>();
public ConcurrentDictionary<string, object> Storage
{ 
get
{
if (_csm.Value != null)
return _csm.Value;
_csm.Value = new ConcurrentDictionary<string, object>();
return _csm.Value;
}
} 
}

ContextualStorageAccessor的生命周期是一个单例。

现在的问题是:每个请求是否有唯一的Value实例?换句话说,我是否需要继续手动为_csm.Value分配默认值?或者我可以依靠应用程序本身的类型(例如,ASP.NET MVC、WCF 等)来处理它?

或者,改写一下:"异步流"的结束在哪里,如果使用AsyncLocal.ValueExecutionContext是否保证每个逻辑调用的唯一值将在逻辑调用结束时自动失效(ASP.NET 在简单方案中null将分配给AsyncLocal.Value

如果您尝试此代码,您将看到每个新的异步流都会创建一个新值。所以答案应该是:是的,每个请求应该有一个唯一的值。

private static readonly AsyncLocal<object> Item = new AsyncLocal<object>();
public static async Task Main()
{
async Task Request()
{
if (Item.Value is {})
{
Console.WriteLine("This should never happen.");
throw new InvalidOperationException("Value should be null here.");
}
Item.Value = new object();
}
await Task.Run(Request); // Just to be sure that Item.Value is initialized once.
await Task.WhenAll(
Task.Run(Request),
Task.Run(Request),
Task.Run(Request),
Task.Run(Request),
Task.Run(Request));
Console.WriteLine("finished");
}

演示

但是我尝试了一个更复杂的示例来确定异步流的结束位置。代码非常简单,但大量使用Console.WriteLine使其有点混乱。

public class Program
{
public static async Task Main()
{
await Task.Run(async () => 
{
Console.WriteLine("Async flow entered...");             
// Init async value
if (Cache.Instance.Item.Value is {})
throw new InvalidOperationException("The async flow has just startet. A value should not be initialized.");
var newValue = new object();
Console.WriteLine($"Create: value = #{RuntimeHelpers.GetHashCode(newValue)}");
Cache.Instance.Item.Value = newValue;
await Foo();
Console.WriteLine("Async flow exitted.");
});
Console.WriteLine("Main finished.nn");
}
private static async Task Foo()
{
Console.WriteLine($"Foo: entered...");
await Bar();
Console.WriteLine($"Foo: getting value...");
var knownValue = Cache.Instance.Item.Value;
Console.WriteLine($"Foo: value = #{RuntimeHelpers.GetHashCode(knownValue)}");
Console.WriteLine($"Foo: exitted.");
}
private static async Task Bar()
{
Console.WriteLine($"Bar: entered...");
await Task.CompletedTask;
Console.WriteLine($"Bar: exitted.");
}
}
public sealed class Cache
{
public static Cache Instance = new Cache();
public AsyncLocal<object> Item { get; } = new AsyncLocal<object>(OnValueChanged);
private static void OnValueChanged(AsyncLocalValueChangedArgs<object> args)
{
Console.WriteLine($"OnValueChanged! Prev: #{RuntimeHelpers.GetHashCode(args.PreviousValue)} Current: #{RuntimeHelpers.GetHashCode(args.CurrentValue)}");
}
}

演示

该代码的输出为:

Async flow entered...
Create: value = #6044116
OnValueChanged! Prev: #0 Current: #6044116
Foo: entered...
Bar: entered...
Bar: exitted.
Foo: getting value...
Foo: value = #6044116
Foo: exitted.
Async flow exitted.
OnValueChanged! Prev: #6044116 Current: #0
Main finished.

值流是预期的。这回答了该值是否分配给默认值的问题 - 是的。异步流在Task.Run完成的地方结束,这就是值被确认为default的点。

但是,如果您将Bar中的await Task.CompletedTask;更改为await Task.Delay(1);,事情就会变得有趣。输出看起来大不相同:

Async flow entered...
Create: value = #6044116
OnValueChanged! Prev: #0 Current: #6044116
Foo: entered...
Bar: entered...
OnValueChanged! Prev: #6044116 Current: #0
OnValueChanged! Prev: #0 Current: #6044116
Bar: exitted.
Foo: getting value...
Foo: value = #6044116
Foo: exitted.
Async flow exitted.
OnValueChanged! Prev: #6044116 Current: #0
Main finished.

OnValueChanged! Prev: #0 Current: #6044116
OnValueChanged! Prev: #6044116 Current: #0

奇怪的部分在输入Bar后开始。看起来await Task.Delay(1)中断了异步流。但价值是相应地恢复的。在这里,我至少可以通过猜测来弥补一个解释。

而真正的插曲发生在主线完成后。该值将再次恢复并清除...我在这里缺乏想象力。我绝对不知道为什么以及如何在完成Task.Run并且异步流也应该完成后恢复该值。这让我感觉GC在程序结束之前无法清除对象。

最新更新