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

时间:2019-08-16 00:31:19

标签: c# async-await

set.seed(1234)
Date <- seq(as.Date("2001/08/01"), by = "month", length.out = 60)
AHD <- rnorm(60, mean = 12, sd = 1)
df <- data.frame(Date=Date, AHD = AHD)

书上写着:

  

但是存在细微的风险。由于Lambda表达式是异步的,   它可以在任何调用Value和表达式的线程上执行   将在上下文中运行。更好的解决方案是将   基础任务中的表达式,它将强制异步   在线程池线程上执行。

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

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

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;              
    }
}

2 个答案:

答案 0 :(得分:3)

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

Lazy<Task<string>> myLazy = new Lazy<Task<string>>(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;
    var result = await myLazy.Value;
    int t2 = Thread.CurrentThread.ManagedThreadId;
    MessageBox.Show($"Before await: #{t1}, {result}, After await: #{t2}");
}

我使用一个按钮将这段代码嵌入到新的Windows Forms应用程序中,然后单击该按钮会弹出此消息:

  

在等待之前:#1,在延迟之前:#1,在延迟之后:#1,在等待之后:#1

然后我将valueFactory参数改为使用Task.Run

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

现在的消息是这样:

  

在等待之前:#1,在延迟之前:#3,在延迟之后:#4,在等待之后:#1

因此,不使用Task.Run意味着await之前,之间和之后的代码将在UI线程中运行。除非在某处隐藏了CPU密集型或IO阻止代码,否则这可能并不重要。例如,Person类的构造函数可能看起来很无辜,可能包含对Web API的一些调用。通过使用Task.Run,您可以确保Lazy类的初始化在完成之前不会碰到UI线程。

答案 1 :(得分:1)

  

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

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

大部分时间

都没关系。但是在某些情况下,可能会发生坏事。引入Task.Run只会消除这种不确定性:委托将始终在线程池线程上没有上下文的情况下运行。