最简单的方法是将调用转储到 Task.Run 中,然后等待 Task.Run 的返回。这会将密集的工作卸载到另一个线程,从而允许 UI 线程继续。这是一个必须等待另一个耗时的方法才能返回的方法的简单示例:
static void Main()
{
Console.WriteLine("Begin");
var result = BlockingMethod("Hi!");
Console.WriteLine("Result: " + result);
Console.ReadLine();
}
static bool BlockingMethod(string someText)
{
Thread.Sleep(2000);
return someText.Contains("SomeOtherText");
}
正如我们所见,BlockingMethod 方法有一个 Thread.Sleep(2000) 作为它的第一条语句。这意味着运行调用方法 Main 的线程必须等待整整两秒钟才能获得 BlockingMethod 的结果。如果运行 Main 方法的线程正在处理 UI 重绘,那么这意味着我们得到的 UI 看起来无响应/锁定了整整两秒钟。我们可以将等待 BlockingMethod 的工作卸载到另一个线程上,如下所示:
首先,我们将调用方法标记为“异步”,因为这告诉编译器为异步方法中的所有“等待”生成类似于状态机的东西:
static async void Main()
{
Console.WriteLine("Begin");
var result = BlockingMethod("Hi!");
Console.WriteLine("Result: " + result);
Console.ReadLine();
}
接下来我们将返回类型'void'变成Task。这允许它被其他线程等待(如果您不关心它,可以将其保留为 async void,但具有返回类型的异步方法需要返回 Task):
static async Task Main()
{
Console.WriteLine("Begin");
var result = BlockingMethod("Hi!");
Console.WriteLine("Result: " + result);
Console.ReadLine();
}
现在我们的方法可以异步运行,并且可以被其他方法等待。但这还没有解决我们的问题,因为我们还在同步等待阻塞方法。所以我们通过调用Task.Run将我们的阻塞方法移到另一个线程,并等待它的结果:
static async Task Main()
{
Console.WriteLine("Begin");
await Task.Run(() => BlockingMethod("Hi!"));
Console.WriteLine("Result: " + result);
Console.ReadLine();
}
但这给我们带来了一个问题:我们的阻塞方法正在返回一些我们想稍后在我们的方法中使用的东西,但是 Task.Run 返回的是 void!为了访问另一个线程中运行的阻塞方法返回的变量,我们必须在闭包中捕获一个局部变量:
static async Task Main()
{
Console.WriteLine("Begin");
bool result = true;
await Task.Run(() => result = BlockingMethod("Hi!"));
Console.WriteLine("Result: " + result);
Console.ReadLine();
}
所以,总而言之,我们所做的是我们采用了一个普通的同步方法(我们可以重写的调用方法,而不是我们无法重写的第三方 API),我们将其更改为将其标记为 async 并且它返回一个任务。如果我们希望它返回一个结果,那么它需要是一个通用类型的任务。之后,我们将对阻塞方法的调用封装在 Task.Run 中——它为阻塞方法创建一个新线程来运行——然后我们给它一个 Action(在 lambda 语法中)运行。在那个动作中,我们引用了一个在调用函数中定义的变量(这是闭包),它可以捕获阻塞方法的结果。
现在,我们在其他地方异步等待同步的阻塞方法。阻塞方法本质上不是异步的并不重要,因为我们让它在另一个线程中同步运行,并允许我们的线程只是等待它的结果。
如果其中任何一个不清楚,请发表评论。异步起初有点令人困惑,但它是响应式 UI 的天赐之物。
编辑:
作为对 Scott Chamberlain 的评论的回应,Task.Run 也有一个重载,它返回其运行的方法的类型(为其提供 Func,而不是 Action)。所以我们可以简单地使用:
static async Task MainAsync()
{
Console.WriteLine("Begin");
bool result = await Task.Run(() => BlockingMethod("Hi!"));
Console.WriteLine("Result: " + result);
Console.ReadLine();
}
请记住——正如 Scott Chamberlain 所指出的——在另一个线程中运行工作并没有固有的性能优势。在许多情况下,它实际上可能会导致性能下降,因为设置和拆除线程的成本很高。基于任务的异步仅在保持繁忙线程(例如 UI 线程)畅通时有用,因此它可以响应请求,或者在通过使用例如 Parallel.ForEach 正确细分工作时。