提问人:John 提问时间:8/16/2019 最后编辑:Theodor ZouliasJohn 更新时间:11/17/2023 访问量:14918
具有异步初始化的 Lazy<Task<T>>
Lazy<Task<T>> with asynchronous initialization
问:
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();
答:
我简化了您的示例,以显示每种情况下会发生什么。在第一种情况下,是使用 lambda 创建的:Task
async
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
然后我更改了要使用的参数: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}";
}));
现在的消息是这样的:
Before await: #1, Before Delay: #3, After Delay: #4, After await: #1
因此,不使用意味着 s 之前、之后和之后的代码将在 UI 线程上运行。这可能没什么大不了的,除非某处隐藏了 CPU 密集型或 I/O 阻塞代码。例如,类的构造函数,尽管它看起来很无辜,但可能包含对数据库或 Web API 的一些调用。通过使用,可以确保类的初始化在完成之前不会触及 UI 线程。Task.Run
await
Person
Task.Run
Lazy
我看不出当前代码有什么风险?
对我来说,主要问题是异步初始化委托不知道它将在哪个上下文/线程上运行,并且上下文/线程可能因竞争条件而异。例如,如果 UI 线程和线程池线程同时尝试访问,则在某些执行中,委托将在 UI 上下文中运行,而在其他执行中,它将在线程池上下文中运行。在 ASP.NET(Core之前)的世界里,它可能会变得有点棘手:委托可能会捕获请求的请求上下文,然后取消(并释放),并尝试在该上下文上恢复,这并不漂亮。Value
大多数时候,这并不重要。但在某些情况下,坏事可能会发生。引入 just 消除了这种不确定性:委托将始终在没有上下文的情况下在线程池线程上运行。Task.Run
评论
Wait()
cmd.ExecuteReaderAsync