到底是 return await Task.Run 还是 return Task.Run?

关于返回值,实际这里有两个层面(本文说的是第二个层面):

  • 一是函数的返回值:

    • void 表示函数无返回值。

    • Task 表示函数返回值是 Task,但是异步无返回值。

  • 二是这个异步的返回值。

    • Task 表示函数返回值是 Task,但是异步无返回值。

    • Task<T> 表示函数返回值是 Task<T>,异步返回值是 T。

为什么要返回 Task,不直接 void?为什么要返回 Task<T>,不直接返回 T?

其实大多数场景确实是可以这样做的,但是有些开发中,比如 handler,返回 Task、Task<T>,目的是让你实现这个 handler 时,必须使用异步去实现。请参见:C# 中 Action 和 Func。

若觉得本文复杂、头晕,直接看最后结论就是了。

先看 Task(无返回值)

private static async Task Do1Async()
{
	await Task.Run(() => {
		Thread.Sleep(3000);
		Console.WriteLine($"Do1Async {DateTime.Now}");
	}); // 此处不能 return await,见后解释。
}


private static Task Do1()
{
	return Task.Run(() => {
		Thread.Sleep(3000);
		Console.WriteLine($"Do1 {DateTime.Now}");
	});
}

如上,Do1Async 中的 Async 是微软推荐 async 修饰时加上的,但并非必须。

  • 内部 await 了,方法修饰符必须是 async

  • Task.Run await 了,可以不用 return,此时它认为返回值类型是 Task<TResult> 中的 TResult,而上述代码并没有指定 TResult,所以不需要返回值(所以上述代码中说:不能 return await)。

  • Task.Run 没有 await,则需要 return,此时它认为返回值类型是 Task

  • 由于用 await 没有用 return,所以 await 后面还可以写语句。

除了上述最后一条外,实际上,上述代码并没有太多区别:

private static void Do()
{
	Console.WriteLine($"a {DateTime.Now}");
	Do1Async(); // 此处 Visual Studio 会警告,说这里会异步,建议用 await(仅仅是建议)。
	Console.WriteLine($"b {DateTime.Now}");
	Do1(); // 此处 Visual Studio 不会警告。
	Console.WriteLine($"end {DateTime.Now}");
}


static void Main(string[] args)
{
	Do();
	Console.WriteLine($"out {DateTime.Now}");
	Console.ReadKey();
}

结果如下:

a 2022-05-04 19:48:26
b 2022-05-04 19:48:26
end 2022-05-04 19:48:26
out 2022-05-04 19:48:26
Do1 2022-05-04 19:48:29
Do1Async 2022-05-04 19:48:29

如果看不惯警告,那么我们加上 await,则 Do 方法也要标记 async,且返回值改为 Task,同时 Visual Studio 建议我们改方法名为 DoAsync

同时 Main 方法调用 DoAsync 也会得到警告。

所以指定了 await,就形成了一个传递链,各层级的调用方,都必须是 await + async Task。

private static async Task DoAsync()
{
	Console.WriteLine($"a {DateTime.Now}");
	await Do1Async(); // 此处 Visual Studio 不会警告。
	Console.WriteLine($"b {DateTime.Now}");
	Do1(); // 此处 Visual Studio 会警告,说这里会异步,建议用 await(仅仅是建议)。我们可以加上,不会报错。
	Console.WriteLine($"end {DateTime.Now}");
}

所以:这里加上 await 是什么意思呢?

如上 Main 调用 DoAsync,输出 a,然后看到 await,于是说:好的,你忙,我先退出去执行其他的了。于是继续执行 Main 中的其他代码(输出 out),再等一会儿,await Do1Async 延时到了,然后输出 Do1Async,然后输出 b,然后输出 end,然后再是输出 Do1(因为 Do1 没有用 await 调用,所以 end 没被阻塞到)。

其实根据我们业务需要吧,如果我就要异步,后面代码我不关心顺序,那完全可以用 return。

再看 Task<TResult>(有返回值)

前面提到了,有无 await 对返回值的解释不同:

  • 有 await:对于 Task<TResult>,必须 return TResult;对于 Task,不能 return。

  • 无 await:对于 Task<TResult>,必须 return Task<TResult>;对于 Task,必须 return Task。

private static async Task<int> Do1Async()
{
	int m = await Task.Run(() => {
		Thread.Sleep(1000);
		return 1;
	});
	return m; // 也可以不要 int m,直接 return await,我们换种写法,以表明 = 与 await 的位置。
}


private static Task<int> Do1()
{
	return Task.Run(() => {
		Thread.Sleep(1000);
		return 2;
	});
}


private static async Task DoAsync()
{
	Console.WriteLine(Do1Async());
	Console.WriteLine(Do1());
}

如上,DoAsync 会得到一个警告,建议 await。

private static async Task DoAsync()
{
	Console.WriteLine(Do1Async());
	Console.WriteLine(await Do1());
}

我们给 Do1() 加上 await,就没有警告了,也就是说在这个调用链条上,要用一个 await,才不会得到警告。

但是,你以为没警告,就正确吗?

private static async Task DoAsync()
{
	Console.WriteLine(Do1Async());
	Console.WriteLine(await Do1());
}

得到的结果是:

System.Threading.Tasks.Task`1[System.Int32]
2

也就是说:在取值的时候,要使用 await,得到的才是 Task<TResult> 中的 TResult,否则它所得到的就是 Task。

于是我们认为 await 有两个作用

1、阻塞我后面的代码(但不阻塞调用者外面的)。

2、使返回值由 Task 变为 Task<TResult> 中的 TResult。

针对第二条,我们要注意 await 应该放在调用的地方,放错地方白搭,比如上面 Do1Async,明明返回的是 int m,可是输出则是 System.Threading.Tasks.Task`1[System.Int32],即 Task<int>。

简简单单

本文前面,其实为了去探究 await、async 及 Task、Task<T> 的返回值,其实是故意把事情弄复杂了。

我们在写返回值类型为 Task、Task<T> 的函数时,就直接 return 就是了,也就是文中的 Do1 写法。

至于 await、async 则在调用它的函数中使用就是了。

你可能感兴趣的