Asp.Net 每个请求的核心异步惰性初始化

Asp.Net Core async lazy initialization per request

对于我们的多租户 Asp.Net Core 2.2 Web 应用程序中的许多端点,我们需要根据每个请求(使用异步数据库调用)从数据库中获取一些租户信息,然后重用该信息根据需要从请求管道中的各个点获取信息,而不必每次都调用数据库。也就是说,我们想要基于每个请求的惰性异步 属性。

根据我阅读的各种文档,scoped 服务的实例一次只能由一个线程(处理请求的线程)访问。所以,我相信这意味着我应该能够安全地执行以下操作:

// Registered as scoped in the DI container
public class TenantInfoRetriever : ITenantInfoRetriever
{
    private TenantInfo _tenantInfo;
    private bool _initialized;

    public async Task<TenantInfo> GetAsync()
    {
        // This code is obviously not thread safe, which is why it's important that no two threads are running here at the same time.
        if (!_initialized)
        {
            _tenantInfo = await GetTenantInfoAsync(); // this might actually legitimately return null, hence the need for the separate property "_initialized"
            _initialized = true;
        }
        return _tenantInfo;
    }

    private async Task<TenantInfo> GetTenantInfoAsync()
    {
        return await DoDatabaseCallToGetTenantInfo(); // of course this would use an injected EfContext instance (scoped service)
    }
}

据我所知,这应该可行,因为作用域服务不需要是线程安全的。

我的问题:我的假设是否正确,或者我是否遗漏了一些重要的东西?

Based on the various docs I've read, instances of scoped services are only ever accessed by one thread at a time (the thread processing the request).

这不完全正确。当您等待一个操作时,线程返回到线程池,当异步操作恢复时,线程池中的一个空闲线程将被拾取。 它不需要启动它的完全相同的线程。

但是,是的,不会有两个线程同时访问它,当它的作用域是从ASP.NET核心端。

当然,你自己的代码也要有所防范。 如果您同时启动两个任务/线程并 运行 它们,那么它仍然有可能同时被访问。

如果您担心它会被访问,请仔细检查锁定模式(使用可变字段)或锁定互斥锁(不是您修改的值的对象)或使用 AsyncLazy<T>,即来自this blog post.

示例(来自 https://help.semmle.com/wiki/display/CSHARP/Double-checked+lock+is+not+thread-safe

示例 1:

string name;

public string Name
{
    get
    {
        lock (mutex)    // GOOD: Thread-safe
        {
            if (name == null)
                name = LoadNameFromDatabase();
            return name;
        }
    }
}

示例 2:

volatile string name;    // GOOD: Thread-safe

public string Name
{
    get
    {
        if (name == null)
        {
            lock (mutex)
            {
                if (name == null)
                    name = LoadNameFromDatabase();
            }
        }
        return name;
    }
}