为什么当我尝试访问任务的 Result 属性时此异步操作会挂起? [英] Why does this async action hang when I try and access the Result property of my Task?

查看:36
本文介绍了为什么当我尝试访问任务的 Result 属性时此异步操作会挂起?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个多层 .Net 4.5 应用程序使用 C# 的新 asyncawait 关键字调用一个方法,这些关键字只是挂起,我不明白为什么.

在底部,我有一个异步方法来扩展我们的数据库实用程序 OurDBConn(基本上是底层 DBConnectionDBCommand 对象的包装器):

public static async TaskExecuteAsync(这个 OurDBConn 数据源,Func 函数){字符串 connectionString = dataSource.ConnectionString;//启动 SQL 并传回调用者直到完成T 结果 = 等待 Task.Run(() =>{//复制 SQL 连接,这样我们就不会在同一个打开的连接上同时运行两个命令使用 (var ds = new OurDBConn(connectionString)){返回函数(ds);}});返回结果;}

然后我有一个中级异步方法调用它来获得一些运行缓慢的总数:

public static async TaskGetTotalAsync(...){var result = await this.DBConnection.ExecuteAsync(ds =>ds.Execute("选择运行缓慢的数据到结果中"));返回结果;}

最后我有一个同步运行的 UI 方法(一个 MVC 操作):

TaskasyncTask = midLevelClass.GetTotalAsync(...);//做其他需要几秒钟的事情ResultClass slowTotal = asyncTask.Result;

问题是它永远挂在最后一行.如果我调用 asyncTask.Wait(),它会做同样的事情.如果我直接运行慢 SQL 方法,大约需要 4 秒.

我期望的行为是,当它到达 asyncTask.Result 时,如果它没有完成,它应该等到它完成,一旦它完成,它就应该返回结果.

如果我使用调试器单步执行,SQL 语句完成并且 lambda 函数完成,但永远不会到达 return result;GetTotalAsync.

知道我做错了什么吗?

对于我需要调查的地方以解决此问题有什么建议吗?

这可能是某个地方的死锁,如果是,有什么直接的方法可以找到它吗?

解决方案

是的,这是一个僵局,好吧.这是 TPL 的常见错误,所以不要难过.

当您编写 await foo 时,默认情况下,运行时会在方法启动所在的同一 SynchronizationContext 上安排函数的继续.在英语中,假设您从 UI 线程调用了 ExecuteAsync.您的查询在线程池线程上运行(因为您调用了 Task.Run),然后您等待结果.这意味着运行时将安排您的return result;"行在 UI 线程上运行,而不是将其安排回线程池.

那么这个僵局是如何产生的呢?想象一下,您只有以下代码:

var task = dataSource.ExecuteAsync(_ => 42);var 结果 = task.Result;

所以第一行开始异步工作.然后第二行阻塞 UI 线程.因此,当运行时想要在 UI 线程上运行返回结果"行时,它不能这样做,直到 Result 完成.但是当然,在返回发生之前不能给出结果.死锁.

这说明了使用 TPL 的一个关键规则:当你在 UI 线程(或其他一些花哨的同步上下文)上使用 .Result 时,你必须小心确保没有任何任务依赖on 被调度到 UI 线程.否则邪恶就会发生.

那你怎么办?选项 #1 到处都使用 await,但正如您所说,这已经不是一个选项.可供您使用的第二个选项是简单地停止使用 await.您可以将两个函数重写为:

public static TaskExecuteAsync(这个 OurDBConn 数据源,Func 函数){字符串 connectionString = dataSource.ConnectionString;//启动 SQL 并传回调用者直到完成返回任务.运行(() =>{//复制 SQL 连接,这样我们就不会在同一个打开的连接上同时运行两个命令使用 (var ds = new OurDBConn(connectionString)){返回函数(ds);}});}公共静态任务GetTotalAsync(...){返回 this.DBConnection.ExecuteAsync(ds =>ds.Execute("选择运行缓慢的数据到结果中"));}

有什么区别?现在没有任何地方等待,因此没有任何内容被隐式安排到 UI 线程.对于像这样只有一个返回值的简单方法,执行var result = await...; return result"模式是没有意义的;只需删除 async 修饰符并直接传递任务对象.如果不出意外,它的开销会更少.

选项#3 是指定您不希望您的等待调度回 UI 线程,而只是调度到线程池.您可以使用 ConfigureAwait 方法执行此操作,如下所示:

public static async TaskGetTotalAsync(...){var resultTask = this.DBConnection.ExecuteAsync(ds =>return ds.Execute("选择运行缓慢的数据到结果中");return await resultTask.ConfigureAwait(false);}

等待任务通常会安排到 UI 线程,如果你在上面的话;等待 ContinueAwait 的结果将忽略您所在的任何上下文,并始终调度到线程池.这样做的缺点是您必须在 .Result 所依赖的所有函数中无处不在,因为任何错过的 .ConfigureAwait 都可能导致另一个死锁.

I have a multi-tier .Net 4.5 application calling a method using C#'s new async and await keywords that just hangs and I can't see why.

At the bottom I have an async method that extents our database utility OurDBConn (basically a wrapper for the underlying DBConnection and DBCommand objects):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

Then I have a mid level async method that calls this to get some slow running totals:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Finally I have a UI method (an MVC action) that runs synchronously:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

The problem is that it hangs on that last line forever. It does the same thing if I call asyncTask.Wait(). If I run the slow SQL method directly it takes about 4 seconds.

The behaviour I'm expecting is that when it gets to asyncTask.Result, if it's not finished it should wait until it is, and once it is it should return the result.

If I step through with a debugger the SQL statement completes and the lambda function finishes, but the return result; line of GetTotalAsync is never reached.

Any idea what I'm doing wrong?

Any suggestions to where I need to investigate in order to fix this?

Could this be a deadlock somewhere, and if so is there any direct way to find it?

解决方案

Yep, that's a deadlock all right. And a common mistake with the TPL, so don't feel bad.

When you write await foo, the runtime, by default, schedules the continuation of the function on the same SynchronizationContext that the method started on. In English, let's say you called your ExecuteAsync from the UI thread. Your query runs on the threadpool thread (because you called Task.Run), but you then await the result. This means that the runtime will schedule your "return result;" line to run back on the UI thread, rather than scheduling it back to the threadpool.

So how does this deadlock? Imagine you just have this code:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

So the first line kicks off the asynchronous work. The second line then blocks the UI thread. So when the runtime wants to run the "return result" line back on the UI thread, it can't do that until the Result completes. But of course, the Result can't be given until the return happens. Deadlock.

This illustrates a key rule of using the TPL: when you use .Result on a UI thread (or some other fancy sync context), you must be careful to ensure that nothing that Task is dependent upon is scheduled to the UI thread. Or else evilness happens.

So what do you do? Option #1 is use await everywhere, but as you said that's already not an option. Second option which is available for you is to simply stop using await. You can rewrite your two functions to:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

What's the difference? There's now no awaiting anywhere, so nothing being implicitly scheduled to the UI thread. For simple methods like these that have a single return, there's no point in doing an "var result = await...; return result" pattern; just remove the async modifier and pass the task object around directly. It's less overhead, if nothing else.

Option #3 is to specify that you don't want your awaits to schedule back to the UI thread, but just schedule to the thread pool. You do this with the ConfigureAwait method, like so:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

Awaiting a task normally would schedule to the UI thread if you're on it; awaiting the result of ContinueAwait will ignore whatever context you are on, and always schedule to the threadpool. The downside of this is you have to sprinkle this everywhere in all functions your .Result depends on, because any missed .ConfigureAwait might be the cause of another deadlock.

这篇关于为什么当我尝试访问任务的 Result 属性时此异步操作会挂起?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆