Avoid async lambda in Parallel
并行与异步是提升程序性能的常用手段。但是应避免在Parallel.For或者Parallel.ForEach中使用异步Lambda函数。
以下通过一个示例说明在Parallel中使用异步Lambda函数的问题。
版本一,初始版本,使用同步方法
假定我们需要在程序中处理一组任务{ "A", "B", "C", "D" }
,处理一个任务的方法简化为DoTask
。一个初始的实现是在DoTasks
中同步执行这些任务。
static void Log(string message)
{
Console.WriteLine("{0} {1} {2}", DateTime.UtcNow.ToString("HH:mm:ss fff"), message);
}
static bool DoTask(string key)
{
Thread.Sleep(1000);
Log(key);
return true;
}
static void DoTasks(string[] keys)
{
foreach (var key in keys)
{
DoTask(key);
}
}
static async Task Main(string[] args)
{
Log("Start");
var keys = new string[] { "A", "B", "C", "D" };
DoTasks(keys);
Log("End");
}
程序运行后,可以得到类似这样的输出:
13:24:58 685 Start
13:24:59 737 A
13:25:00 738 B
13:25:01 740 C
13:25:02 741 D
13:25:02 741 End
可见程序运行花了4秒多一点儿。
版本二,并行处理任务
通过使用Parallel.ForEach可以同时执行多个任务。
static void DoTasksParallel(string[] keys)
{
Parallel.ForEach(keys, key =>
{
DoTask(key);
});
}
程序运行后,得到输出:
13:28:12 670 Start
13:28:13 856 C
13:28:13 856 B
13:28:13 863 A
13:28:13 864 D
13:28:13 864 End
可见并行提升了程序运行速度,这次只花了1秒多点儿。
版本三,并行中使用异步
假如处理任务的过程中有异步操作,我们很可能会实现一个新的DoTaskAsync
方法以提升程序性能。
static async Task<bool> DoTaskAsync(string key)
{
await Task.Delay(1000);
Log(key);
return true;
}
static void DoTasksParallelAsync(string[] keys)
{
Parallel.ForEach(keys, async key =>
{
await DoTaskAsync(key);
});
}
这次程序很快就运行结束了,但是并没有输出执行任务的日志。
13:39:13 890 Start
13:39:14 072 End
可见,当程序结束时,任务还没有完成。如果我们在Main
函数最后加入Console.ReadLine();
等待输入,再次运行程序会得到这样的输出:
13:43:30 133 Start
13:43:30 312 End
13:43:31 321 D
13:43:31 321 C
13:43:31 321 A
13:43:31 321 B
这说明Parallel.ForEach没有等待 async lambda 运行结果就执行后续操作了。通常,这不是我们想要的程序行为。
问题在于 async lambda 会被转换成 async void 函数,而 async void 函数应慎用。因为它的返回值是void而不是Task,没有简单的方法可以让调用者知道它是否结束了。
避免在Parallel中使用 async lambda 函数。
版本四,异步任务
其实对于执行异步操作的任务来说,可以直接获取到对应的Task,然后等待任务运行结束,不一定需要使用Parallel。
static async Task DoTasksAsync(string[] keys)
{
var tasks = keys.Select(x => DoTaskAsync(x));
await Task.WhenAll(tasks);
}
程序运行后,得到输出:
14:05:27 255 Start
14:05:28 418 B
14:05:28 418 A
14:05:28 418 D
14:05:28 418 C
14:05:28 422 End
可以看到程序运行时间也是1秒多点儿。
如果任务中有CPU密集计算,也可以使用Task.Run等方法结合Parallel实现并行。