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只是消除了这种不确定性:委托将始终在没有线程池线程上下文的情况下运行。