我需要在逻辑请求结束时手动将 AsyncLocal 变量的值更改为“Dispose”/“Release”吗

问题描述 投票:0回答:2

我从

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

我正在研究诸如上下文绑定的缓存/记忆之类的东西,其目的很简单,就是跨逻辑请求存储数据。这类似于旧的

HttpContext.Current
,其中数据跨请求存储,并将在请求结束时释放。然而,就我而言,我希望与环境无关,因此实现并不局限于例如ASP.NET MVC、ASP.NET Core、WCF 等,同时仍然能够存储和检索绑定到逻辑请求的数据,而无需在逻辑上不同的请求之间共享数据。

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

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 等)来处理它?

或者,换句话说:“异步流”的结尾在哪里,

ExecutionContext
是否保证每个逻辑调用的唯一值将自动失效——在一个简单的场景中,
null
将被分配给
AsyncLocal.Value
——通过如果使用
AsyncLocal.Value
,逻辑调用结束(对于 ASP.NET MVC,是一个 Web 请求;对于 WCF,是一个操作)?

c# asynchronous caching concurrency async-await
2个回答
6
投票

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

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.\n\n");
    }

    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
的点。

但是如果将

await Task.CompletedTask;
中的
Bar
更改为
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)
破坏了异步流程。但价值已正确恢复。在这里我至少可以通过猜测来做出一个解释。

真正有趣的事情发生在 main 完成之后。数值再次恢复并清除……我这里缺乏想象力。我完全不知道为什么以及如何在

Task.Run
完成后恢复该值,并且异步流程也应该完成。这给我的感觉是GC在程序结束之前无法清除对象。


0
投票

就像@Sebastian Schumann提到的那样,GC似乎不会在触发异步流程的方法结束后立即发生。这似乎破坏了基于工作服务的应用程序中的我的对象。即使在手动清除 Context.Value 后,我也可以获取上次重复存储的对象,因为该对象已经超出了范围。

您可以在这里查看我的问题,它还包含一个 MRE。

© www.soinside.com 2019 - 2024. All rights reserved.