首页 > 解决方案 > 使用 Task.Result 时防止 UI 冻结

问题描述

我正在调用 Task.Run(() => DoSomething()).Result 这会导致 UI 冻结并且它发生是因为正在使用“.Result”。我需要 Result 因为我想返回值。

我不想让 StartSomething 方法异步,因为我不想等待 StartSomething 方法。我希望等待发生在 DoSomething()。

所以基本上我需要一个由同步方法调用的异步方法,而不会冻结 UI。另外,我想将异步方法中的值返回到按钮单击的顶层。

可以改进此代码还是有其他解决方案?

private TaskCompletionSource<bool> TaskCompletion = null;
private void Button_Click(object sender, RoutedEventArgs e)
    {
        bool k = StartSomething();
    }

    private bool StartSomething()
    {
        return Task.Run(() => DoSomething()).Result;
    }

    private async Task<bool> DoSomething()
    {
        TaskCompletion = new TaskCompletionSource<bool>();
        await Task.WhenAny(TaskCompletion.Task, Task.Delay(3000));
        MessageBox.Show("DoSomething");
        return true;
    }

标签: c#.nettask

解决方案


方法StartSomething()对我没有意义。它开始一个新的Task,然后只是同步地等待.Result这个任务的结果(),这实际上是没有用的——它几乎[*]与直接调用相同DoSomething()。也DoSomething()已经是异步的,所以你不需要Task为它开始一个新的。

看起来你根本不需要StartSomething()方法。如果您制作Button_Clickhandler async,您可以await DoSomething()直接直接:

private TaskCompletionSource<bool> TaskCompletion = null;

private async void Button_Click(object sender, RoutedEventArgs e)
{
    bool k = await DoSomething();
}

private async Task<bool> DoSomething()
{
    TaskCompletion = new TaskCompletionSource<bool>();
    await Task.WhenAny(TaskCompletion.Task, Task.Delay(3000));
    MessageBox.Show("DoSomething");
    return true;
}


编辑:

虽然一直使用异步解决方案(如上所示)是 IMO 的首选方式,但如果您真的无法将调用代码更改为async,我可以考虑两种async方法从同步方法调用方法而不阻塞 UI。首先是手动设置这样的延续任务:

private void Button_Click(object sender, RoutedEventArgs e)
{
    DoSomething().ContinueWith((task) =>
        {
            bool k = task.Result;

            // use the result
        },

        // TaskScheduler argument is needed only if the continuation task
        // must run on the UI thread (eg. because it access UI elements).
        // Otherwise this argument can be omitted.
        TaskScheduler.FromCurrentSynchronizationContext());

    // Method can exit before DoSomething().Result becomes
    // available, which keep UI responsive
}

因此,您基本上将同步方法(一个拆分而不是每个拆分await)拆分为几个部分(延续 lambda 方法),由.ContinueWith. 这类似于await引擎盖下的操作。问题在于,与await(生成漂亮而干净的代码)不同的是,您的代码将充满这些延续 lambda。using当你添加异常处理块、块等时,情况会变得更糟。

第二种方法是使用嵌套循环,例如。Stephen Toub 的WaitWithNestedMessageLoop扩展方法:

static T WaitWithNestedMessageLoop<T>(this Task<T> task)
{
    var nested = new DispatcherFrame();
    task.ContinueWith(_ => nested.Continue = false, TaskScheduler.Default);
    Dispatcher.PushFrame(nested);
    return task.Result;
}

嵌套循环是非常先进的技术(我实际上从未使用过),除非您必须这样做,否则我不建议使用它。


[*]在异常处理、执行线程等方面存在差异,但这些与本题无关。


推荐阅读