具有异步初始化的 Lazy<Task<T>>

Lazy<Task<T>> with asynchronous initialization

提问人:John 提问时间:8/16/2019 最后编辑:Theodor ZouliasJohn 更新时间:11/17/2023 访问量:14918

问:

class Laziness
{
    static string cmdText = null;
    static SqlConnection conn = null;

 
    Lazy<Task<Person>> person =
        new Lazy<Task<Person>>(async () =>      
        {
            using (var cmd = new SqlCommand(cmdText, conn))
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                if (await reader.ReadAsync())
                {
                    string firstName = reader["first_name"].ToString();
                    string lastName = reader["last_name"].ToString();
                    return new Person(firstName, lastName);
                }
            }
            throw new Exception("Failed to fetch Person");
        });

    public async Task<Person> FetchPerson()
    {
        return await person.Value;              
    }
}

Riccardo Terrell 于 2018 年 6 月出版的《.NET 中的并发性》一书说:

但有一个微妙的风险。由于 Lambda 表达式是异步的, 它可以在调用 Value 和表达式的任何线程上执行 将在上下文中运行。更好的解决方案是包装 表达式,这将强制异步 在线程池线程上执行。

我看不出当前代码有什么风险?

是为了防止代码在UI线程上运行并且像这样显式等待时出现死锁:

new Laziness().FetchPerson().Wait();
C# 异步 async-await 惰性评估

评论

0赞 Patrick Roberts 8/16/2019
即使基础任务在不同的线程上运行,这仍然会在任务期间阻塞 UI...Wait()
2赞 TheGeneral 8/16/2019
此代码目前没有风险,但是建议将异步 lambda 卸载到线程池线程,我认为在这种情况下这是矫枉过正和浪费线程
0赞 Theodor Zoulias 8/16/2019
@TheGeneral只有在内部有同步代码的情况下才会浪费线程,这是极不可能的。预期的情况是线程将执行少量 CPU 指令,然后返回到线程池。cmd.ExecuteReaderAsync
1赞 johnny 5 8/16/2019
你为什么还要使用懒惰?只是做一个函数,并在需要时调用它?
0赞 Chef Gladiator 7/30/2022
devblogs.microsoft.com/pfxteam/asynclazyt 那是从2011年开始的。八年前。

答:

11赞 Theodor Zoulias 8/16/2019 #1

我简化了您的示例,以显示每种情况下会发生什么。在第一种情况下,是使用 lambda 创建的:Taskasync

Lazy<Task<string>> myLazy = new(async () =>
{
    string result = $"Before Delay: #{Thread.CurrentThread.ManagedThreadId}";
    await Task.Delay(100);
    return result += $", After Delay: #{Thread.CurrentThread.ManagedThreadId}";
});

private async void Button1_Click(object sender, EventArgs e)
{
    int t1 = Thread.CurrentThread.ManagedThreadId;
    string result = await myLazy.Value;
    int t2 = Thread.CurrentThread.ManagedThreadId;
    MessageBox.Show($"Before await: #{t1}, {result}, After await: #{t2}");
}

我使用一个按钮将此代码嵌入到一个新的 Windows 窗体应用程序中,单击该按钮时会弹出此消息:

Before await: #1, Before Delay: #1, After Delay: #1, After await: #1  

然后我更改了要使用的参数:valueFactoryTask.Run

Lazy<Task<string>> myLazy = new(() => Task.Run(async () =>
{
    string result = $"Before Delay: #{Thread.CurrentThread.ManagedThreadId}";
    await Task.Delay(100);
    return result += $", After Delay: #{Thread.CurrentThread.ManagedThreadId}";
}));

现在的消息是这样的:

Before await: #1, Before Delay: #3, After Delay: #4, After await: #1  

因此,不使用意味着 s 之前、之后和之后的代码将在 UI 线程上运行。这可能没什么大不了的,除非某处隐藏了 CPU 密集型或 I/O 阻塞代码。例如,类的构造函数,尽管它看起来很无辜,但可能包含对数据库或 Web API 的一些调用。通过使用,可以确保类的初始化在完成之前不会触及 UI 线程。Task.RunawaitPersonTask.RunLazy

11赞 Stephen Cleary 8/19/2019 #2

我看不出当前代码有什么风险?

对我来说,主要问题是异步初始化委托不知道它将在哪个上下文/线程上运行,并且上下文/线程可能因竞争条件而异。例如,如果 UI 线程和线程池线程同时尝试访问,则在某些执行中,委托将在 UI 上下文中运行,而在其他执行中,它将在线程池上下文中运行。在 ASP.NET(Core之前)的世界里,它可能会变得有点棘手:委托可能会捕获请求的请求上下文,然后取消(并释放),并尝试在该上下文上恢复,这并不漂亮。Value

大多数时候,这并不重要。但在某些情况下,坏事可能会发生。引入 just 消除了这种不确定性:委托将始终在没有上下文的情况下在线程池线程上运行。Task.Run

评论

0赞 Shahar Shokrani 4/1/2020
嘿@Stephen,很好的答案,你为什么提到“pre-Core”括号?
2赞 Stephen Cleary 4/2/2020
因为 ASP.NET 没有环境请求上下文。Pre-Core ASP.NET 做到了,所以存在一个问题,即上下文可以被处置,然后代码尝试使用它。
0赞 Christophe Devos 10/25/2021
您是否有可能对当前文化有问题?假设在 ASP.NET(Core之前)中,每个请求都使用不同的语言/区域性(取决于当前用户),那么这可能会在当前区域性中引发异常,从而导致异常本地化。或取决于区域性的函数的其他问题(解析/格式化)
0赞 Stephen Cleary 10/26/2021
@ChristopheDevos:是的,这是可能的。我的类型包括一个标志,由于这个原因,它避免了。AsyncLazy<T>Task.Run
0赞 Chef Gladiator 7/30/2022
devblogs.microsoft.com/pfxteam/asynclazyt 那是从2011年开始的。八年前。