【问题标题】:Prevent UI from freezing when using Task.Result使用 Task.Result 时防止 UI 冻结
【发布时间】:2018-11-16 05:37:52
【问题描述】:

我正在调用 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;
    }

【问题讨论】:

  • BackgroundWorker 是你的朋友
  • 如果你想学习多任务处理,我也建议使用 WindowsForms 等 GUI 技术中的 BackgroundWorker。任务、线程、线程池——所有这些都是高级技术。在尝试之前,您需要学习基础知识。
  • Task.Run(...).Result;。我不会说它 100% 总是错误的。也许只有 99.99999%。它是“任务系统,请在我(当前线程)做其他事情时找到一个合适的线程来运行这段代码。现在,在我做其他事情之前,我需要运行该代码的结果完成”。这与仅在当前线程上自己运行该代码的结果相同,但会抛出额外的不必要的机器

标签: c# .net task


【解决方案1】:

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

看起来你根本不需要StartSomething() 方法。如果你把Button_Click处理程序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;
}


编辑:

虽然使用 async all way down 解决方案(如上所示)是 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)拆分为由 .ContinueWith 链接的几个部分(延续 lambda 方法)。这类似于 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;
}

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


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

【讨论】:

  • 你做了我不想你做的事。您使 buttonClick 异步。我不希望调用 DoSomething 的方法具有异步修饰符。 @Ňuf
  • @JerinSebastian:你能解释一下,为什么你不想要asyncButton_Click?因为如果你想调用异步方法,经验法则是async all the way down。如果你想保持你的 UI 响应,async 是你最好的朋友。
  • 在我的实际应用中,它不会是一个按钮点击事件,而是一个方法,它会从很多地方被调用。我不想在我使用的任何地方指定异步修饰符。
  • @JerinSebastian:我添加了两种从同步方法调用异步方法而不阻塞 UI 的方法。但是在设计软件时,IMO “我不想指定异步修饰符” 是非常糟糕的论点,特别是如果 async 修饰符正是您所需要的。但我知道,也许您已经拥有数百万行现有代码,将其全部更改为异步可能成本太高,因此可能需要同步到异步调用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-21
  • 2018-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多