使用async await仍然会冻结GUI

15

我希望在单独的线程中处理长时间运行的操作,并使用async/await模式尽快将控制权返回到GUI线程,代码如下:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    await Test();
    txtResult.Text = "Done!";
}

private Task Test()
{
    Thread.Sleep(3000);
    return Task.FromResult(0);
}

问题是,无论如何都会冻结GUI三秒钟(直到三秒后显示“完成!”之前一直不可响应)。我做错了什么?

编辑: 我试图替换以下逻辑:

private void Button_Click(object sender, RoutedEventArgs e)
{
    var thread = new Thread(() => Test(Callback));
    thread.Start();
}

private void Callback()
{
    Dispatcher.Invoke(() =>
        txtResult.Text = "Done!");
}

private void Test(Action callback)
{
    Thread.Sleep(3000); //long running operation, not necessarily pause
    callback();
}

在实际项目中,我有不同于Sleep的长时间运行逻辑,但它仍然会使GUI冻结,因此用Task.Delay替换它并不能解决任何问题。此外,我不明白为什么你应该使用另一个命令来代替Sleep?这与async/await设计有什么关系呢?


4
async并不会导致你的代码在一个单独的线程上运行。它允许当前线程不被阻塞,当它用于创建一个在另一个线程上运行的任务或执行IO绑定任务时,你希望等待该任务完成。然而,你的方法既不创建另一个线程上的任务,也不执行IO绑定任务。 - juharr
2个回答

18
您可以使用 Task.RunTask.Factory.StartNew 在另一个线程上执行 Test() 或某些长时间运行和/或阻塞操作:
private async void Button_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(() => Test());
    txtResult.Text = "Done!";
}

4
对不起,为什么应该使用 await Task.Run(() => Test()); 而不仅仅是 await Test()? - eugenekr
4
Test()返回一个同步任务,并简单地等待它不会在单独的线程上启动该任务。 文档会更好地解释这一点:“async和await关键字不会导致创建额外的线程。”和“您可以使用Task.Run将CPU绑定工作移动到后台线程。” - Nick
Task.Run是任务并行库的一部分。在这种情况下,它与await很好地配合使用。在C#中有很多方法可以在后台执行某些操作。我建议阅读这篇快速指南以获得更好的理解 https://markheath.net/post/starting-threads-in-dotnet - Deepscorn
这个答案帮了我,谢谢你的问题和回答 :-) - Slamit

3

您正在使用阻塞线程的Thread.Sleep方法。请改用Task.Delay,它在内部使用计时器并异步地将控制权返回给调用者:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    await TestAsync();
    txtResult.Text = "Done!";
}

private async Task<int> TestAsync()
{
    await Task.Delay(3000);
    return 0;
}

编辑

由于您发布了新的代码,我建议您隔离需要在日志运行过程后运行的回调函数,并保存Dispatcher.Invoke,并使用async-await代替它,这将让您落入正确的同步上下文中:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(Test);
    CallBack();
}

private void Callback()
{
    txtResult.Text = "Done!"
}

private void Test()
{
    Thread.Sleep(3000); //long running operation, not necessarily pause
}

1
感谢您的回复,在实际项目中,我有一个长时间运行的逻辑,而不是Sleep,请参见编辑后的问题。 - eugenekr
好的,所以 await Task.Run(Test) 异步运行,而 await Task 同步运行?和直接调用 Task() 相比,await Task() 有什么意义? - eugenekr
@EugeneKr “await”仅是一个关键字,用于向编译器发出简单提示。异步操作不是由“await”完成的,而是由被等待的底层方法完成的。这意味着,例如,如果您正在使用具有自然异步方法的“HttpClient”进行异步IO,则当您等待其中的一个方法时,控制权将返回到调用方法。当您使用“Task.Run”时,您正在显式地在线程池线程上运行委托,这与使用自然异步API不同,后者不需要额外的线程。 - Yuval Itzchakov
1
我开始理解了...我想。但仍然看不出它有什么用处。如果我面前有一个使用await Test()的示例代码片段,那就容易多了,如果可能的话,类似于我提供的那些。希望你还有一点耐心来做这件事 :) 我几乎看不出使用await Task.Delay的示例有什么用处。 - eugenekr
@EugeneKr 我不太确定你的意思。根据你的示例,await Test() 并没有异步执行任何操作。它有一个阻塞部分,使用 Thread.Sleep 进行模拟,并使用实用程序 Task.FromResult,它也没有执行任何异步操作,只是使用 TaskCompletionSource<T> 创建了一个已完成的 Task 对象。等待它不会做任何事情,只会阻塞调用线程。 - Yuval Itzchakov
不一定要使用我的示例,我只是在寻找一个有用的await用法的简单示例,而不需要Task.Run。我已经研究了几篇文章,但仍然不明白async/await有什么用处。也许最好创建一个新问题“async/await用法的简单示例”? - eugenekr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接