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

Joh*_*ohn 15 c# asynchronous lazy-evaluation async-await

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;              
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我不明白当前代码有什么风险?

是否是为了防止死锁,以防代码在 UI 线程上运行并像这样显式等待:

new Laziness().FetchPerson().Wait();
Run Code Online (Sandbox Code Playgroud)

The*_*ias 11

我简化了您的示例以展示每种情况下发生的情况。在第一种情况下,Task是使用asynclambda 创建的:

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}");
}
Run Code Online (Sandbox Code Playgroud)

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

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}");
}
Run Code Online (Sandbox Code Playgroud)

然后我改变了valueFactory参数来代替使用Task.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}";
}));
Run Code Online (Sandbox Code Playgroud)

现在消息是这样的:

Before await: #1, Before Delay: #1, After Delay: #1, After await: #1  
Run Code Online (Sandbox Code Playgroud)

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


Ste*_*ary 11

我不明白当前代码有什么风险?

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

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

  • 因为 ASP.NET 没有环境请求上下文。Pre-Core ASP.NET 做到了,因此存在一个问题:可以释放上下文,然后代码尝试使用它。 (2认同)