【问题标题】:Why is TaskCanceledException thrown when using Dapper QueryAsync<T> without async/await?为什么在没有 async/await 的情况下使用 Dapper QueryAsync<T> 时会抛出 TaskCanceledException?
【发布时间】:2016-09-03 00:11:43
【问题描述】:

为什么在这个方法上调用.Result会导致TaskCanceledException:

public Task<IEnumerable<object>> GetAsync()
{
    using (var conn = new SqlConnection("connectionString"))
    {
        return conn.QueryAsync<object>("select * from objects");
    }
}

但在此方法上调用 .Result 有效:

public async Task<IEnumerable<object>> GetAsync()
{
    using (var conn = new SqlConnection("connectionString"))
    {
        return await conn.QueryAsync<object>("select * from objects");
    }
}

区别在于async\await关键字用在第二种方法中。

【问题讨论】:

    标签: c# async-await task dapper


    【解决方案1】:

    第一个方法启动查询(调用QueryAsync),然后处理SqlConnection,然后返回代表该查询的任务。该任务已取消,因为 SqlConnection 在完成之前已被释放。

    第二种方法启动查询(调用QueryAsync),异步等待该查询完成,然后释放SqlConnection。第二种方法返回的任务代表该方法的完成。

    有关async/await 的更多信息,请参阅my blog

    附带说明,您不应该使用 Result 的异步方法;你应该改用await

    【讨论】:

    • 我意识到Result 通常是使用异步方法的错误方式;特别是当 SynchronizationContext 不为空时。但在这种情况下,它是空的,因为我在单元测试项目中进行一些临时测试时遇到了这种情况,只是想快速查看结果,我认为这是对 Result 的有效使用。
    【解决方案2】:

    第一个方法抛出异常的原因是当方法返回Task时SqlConnection对象超出范围。

    第二种方法起作用的原因是因为 async 和 await 关键字模拟了一个闭包,因为编译器在后台将该方法包装在一个类似状态机的结构中。这最终将SqlConnection 保持在范围内。我不会比这更进一步,因为细节有点复杂,并且因编译器而异。

    要使返回一个没有 async/await 关键字的任务成为可能,您需要传入 SqlConnection 以便在返回任务时它保持在范围内:

    public Task<IEnumerable<object>> GetAsync(SqlConnection conn)
    {
        return conn.QueryAsync<object>("select * from objects");
    }     
    

    更新#1(回应评论)

    为什么它与作用域和闭包有关:

    using 语句中实例化SqlConnection 将其范围 限制在using 块中。因此,当第一个方法离开 using 块并返回任务时,SqlConnection 超出范围并在任务完成之前被处置。在第二种方法中,async/await 关键字将导致编译器在幕后创建一个struct,并将异步方法的局部变量作为字段存储在该结构中,以便它们可以在回调中使用(await 下面的代码) 任务完成时。这类似于 closures 在幕后实现的方式。

    【讨论】:

    • 不,行为上的差异与作用域或类似闭包的对象没有任何关系。
    • @StephenCleary 是的,确实如此。见更新#1。似乎您的回答是在说同样的事情,但要高一级。
    • 我认为我们在这里对“范围”的定义存在分歧。对于这两种方法,SqlConnection 的范围是相同的:它只在 using 块内。
    • 我同意你关于SqlConnection范围的说法。我认为我们只是在这里处理不同级别的抽象。当使用async/await的方法被编译时,使用块实际上会消失,内部结构将有一个引用SqlConnection的字段,它将在运行回调后调用Dispose()。我想我的目标是解释 SqlConnection 在使用 async/await 时如何保持在幕后范围内。
    猜你喜欢
    • 1970-01-01
    • 2017-09-08
    • 2022-11-15
    • 2018-07-15
    • 2020-06-18
    • 1970-01-01
    • 2020-03-15
    • 2012-04-23
    • 1970-01-01
    相关资源
    最近更新 更多