C# 试题 33:Task 与 async/await
0641 以下代码输出什么?
难度: 实战
static async Task M() => throw new InvalidOperationException("early");
Task t = M();
Console.WriteLine(t.IsFaulted);
- A. 抛
InvalidOperationException,async 方法同步抛异常 - B.
True,async 方法体内异常被捕获进返回的 Task - C.
False - D. 编译失败
查看答案与解析
正确答案B
解析: async 方法(哪怕异常发生在第一个 await 之前)不会同步抛出,而是把异常捕获进返回的 Task;调用方通过 await 或观察 Task 才能拿到异常。IsFaulted 为 True 而调用处无异常。
关键点: async 方法的异常“迟到”,永远在返回的 Task 里。
0642 以下代码输出什么?
难度: 进阶
async Task M()
{
Console.Write("a");
await Task.CompletedTask;
Console.Write("b");
}
await M();
Console.Write("c");
- A.
acb - B.
a后挂起,永不继续 - C.
abc,已完成任务的 await 不产生让步,同步继续 - D.
ab,最后一行不执行
查看答案与解析
正确答案C
解析: await 只在等待尚未完成的操作时才让步。Task.CompletedTask 已完成,await 直接内联继续,b 在 c 之前打印,输出 abc。想要强制让步应使用 Task.Yield()。
关键点: “await 一定异步”是误解;已完成 await 是同步续行。
0643 以下代码输出什么?
难度: 进阶
static async Task M()
{
Console.WriteLine("sync");
}
await M();
Console.WriteLine("done");
- A. 编译失败,async 方法必须有 await
- B. 只输出
done - C. 抛
InvalidOperationException - D. 编译警告 CS1998,输出
sync/done,方法体同步执行
查看答案与解析
正确答案D
解析: 没有 await 的 async 方法仍能编译,但编译器给出 CS1998 警告;方法体同步执行完毕,返回已完成的 Task。这类“假异步”往往是遗留了 async 修饰符,应及时清理。
关键点: async 无 await = 编译警告 + 完全同步执行。
0644 以下代码运行结果是什么?
难度: 实战
async void Fire() => throw new InvalidOperationException("boom");
try { Fire(); }
catch (Exception) { Console.WriteLine("caught"); }
await Task.Delay(100);
- A. 输出
caught,async void 的异常可被调用方捕获 - B. 异常未被捕获,进程终止(async void 的异常无法由调用方捕获)
- C. 编译失败
- D. 输出
caught后正常结束
查看答案与解析
正确答案B
解析: async void 不返回 Task,异常无处安放,只能抛到当前 SynchronizationContext(或线程池)上成为未处理异常,调用方的 try/catch 永远捕不到,进程会因此终止。async void 只允许用于事件处理器。
关键点: async void = 异常不可观察 + 进程崩溃风险。
0645 以下代码输出什么?
难度: 进阶
Task<int> t = Task.FromException<int>(new InvalidOperationException("x"));
try { _ = t.Result; }
catch (Exception e) { Console.WriteLine(e.GetType().Name); }
- A.
InvalidOperationException - B.
TaskCanceledException - C.
AggregateException,Result/Wait 包装异常,await 才会解包 - D. 编译失败
查看答案与解析
正确答案C
解析: Task.Result 与 Task.Wait() 在任务故障时抛 AggregateException(把内部异常包一层);只有 await(或 GetAwaiter().GetResult())才会把内部异常直接抛出。想少一层包装就避免同步阻塞。
关键点: Result 抛 AggregateException 是新手与资深都常记反的差异。
0646 以下代码输出什么?
难度: 进阶
Task<int> t = Task.FromException<int>(new InvalidOperationException("x"));
try { _ = t.GetAwaiter().GetResult(); }
catch (Exception e) { Console.WriteLine(e.GetType().Name); }
- A.
AggregateException - B.
TaskCanceledException - C. 编译失败
- D.
InvalidOperationException,GetAwaiter().GetResult() 与 await 一样解包一层
查看答案与解析
正确答案D
解析: GetAwaiter().GetResult() 的异常语义与 await 一致:直接抛出内部异常,不包 AggregateException;同时它在已完成的 Task 上调用不会死锁,是“同步取结果”的推荐写法。
关键点: 同步取结果用 GetResult,别用 Result/Wait。
0647 以下代码输出什么?
难度: 实战
Task all = Task.WhenAll(Array.Empty<Task>());
Console.WriteLine(all.IsCompletedSuccessfully);
- A.
True,空集合的 WhenAll 立即成功完成 - B.
False,永远不会完成 - C. 抛
ArgumentException - D. 编译失败
查看答案与解析
正确答案A
解析: Task.WhenAll 接收空集合时返回已成功完成的任务(没有可等待的东西,立即成功)。写“批量等待”的通用代码时不需要对空集合特判。
关键点: WhenAll 对空输入是“立即成功”,不是挂起也不是异常。
0648 以下代码输出什么?
难度: 进阶
Task t = Task.FromException(new InvalidOperationException("boom"));
try { await Task.WhenAll(t); }
catch (Exception e) { Console.WriteLine(e.GetType().Name); }
- A.
AggregateException - B.
InvalidOperationException,await WhenAll 时解包为内部异常 - C.
TaskCanceledException - D. 编译失败
查看答案与解析
正确答案B
解析: WhenAll 聚合所有任务的异常放进 AggregateException,但 await 它时只解包一层、直接抛第一个内部异常;只有访问 all.Exception 才能看到完整聚合。
关键点: await 解包、Exception 属性聚合,两者要分清。
0649 以下代码能编译吗?
难度: 实战
object gate = new();
lock (gate)
{
await Task.Delay(1);
}
- A. 正常运行
- B. 运行抛
SynchronizationLockException - C. 编译失败,不能在 lock 语句体中 await(应使用 SemaphoreSlim 等异步原语)
- D. 死锁
查看答案与解析
正确答案C
解析: 语言层面禁止在 lock 语句体内使用 await(编译错误 CS1996),因为锁要由同一线程释放而 await 会离开线程。跨 await 的互斥必须用 SemaphoreSlim.WaitAsync 等异步同步原语。
关键点: lock 与 await 不能共存,Monitor 不是异步原语。
0650 以下代码能编译吗?
难度: 进阶
static async Task<int> M()
{
yield return 1;
}
- A. 编译失败,async 方法不能包含 yield return(除非返回 IAsyncEnumerable 等异步流)
- B. 正常运行
- C. 运行抛
InvalidOperationException - D. 返回一个元素的 Task
查看答案与解析
正确答案A
解析: 普通 async Task 方法中不能使用 yield;只有返回 IAsyncEnumerable<T>/IAsyncEnumerator<T> 的 async 迭代器才能同时使用 await 与 yield(C# 8+ 异步流)。两者状态机不兼容。
关键点: 异步流是“async + IAsyncEnumerable”,不是“async Task + yield”。
0651 WinForms/WPF 的 UI 线程上执行以下代码,会发生什么?
难度: 实战
private void Button_Click(object sender, EventArgs e)
{
WorkAsync().Wait();
}
async Task WorkAsync()
{
await Task.Delay(10);
}
- A. 正常完成
- B. UI 线程死锁:UI 线程阻塞等待任务,而任务的延续要回到被阻塞的 UI 线程
- C. 抛
TaskCanceledException - D. 编译失败
查看答案与解析
正确答案B
解析: UI 线程有 SynchronizationContext,await 的延续要回 UI 线程执行;.Wait() 又把 UI 线程阻塞住,两者互相等待形成死锁。控制台/ASP.NET Core 没有该上下文,同样的代码不会死锁——但依然不推荐同步阻塞。
关键点: 同步阻塞 + 同步上下文 = 死锁;全程 await 或 ConfigureAwait(false)。
0652 关于 ConfigureAwait(false) 的正确说法?
难度: 进阶
- A. 让异步方法在调用线程同步执行
- B. 只影响 .NET Framework
- C. 让延续不回到捕获的 SynchronizationContext;没有同步上下文(如控制台、ASP.NET Core)时基本无效果
- D. 会取消任务
查看答案与解析
正确答案C
解析: ConfigureAwait(false) 告诉当前 await:后续延续不必回到捕获的 SynchronizationContext。在控制台或 ASP.NET Core(默认无同步上下文)中它几乎没有实际差异;在 UI/旧 ASP.NET 场景它能避免死锁并减少上下文切换。
关键点: 它的作用对象是“延续的上下文”,不是“任务本身”。
0653 以下代码输出什么?
难度: 实战
static async Task<int> Inner()
{
await Task.Delay(1);
return 42;
}
Task t = Task.Run(Inner);
Console.WriteLine(t is Task<int>);
- A.
False,Task.Run 返回Task<Task<int>> - B. 编译失败
- C. 抛
InvalidOperationException - D.
True,Task.Run 会展开内层任务,返回Task<int>
查看答案与解析
正确答案D
解析: Task.Run(Func<Task<TResult>>) 有专门的“展开(unwrap)”语义:直接返回内层任务的任务类型,不会出现 Task<Task<int>> 嵌套。这与 Task.Factory.StartNew 不同(后者需要手动 Unwrap)。
关键点: Task.Run 自动展开,StartNew 不展开。
0654 控制台应用(无同步上下文)中,以下代码输出什么?
难度: 实战
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1);
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
- A.
False/True,await 后的延续在线程池上运行 - B.
True/True - C.
False/False - D.
True/False
查看答案与解析
正确答案A
解析: 主线程不是线程池线程;await 未完成的操作后,控制台应用没有同步上下文,延续被调度到线程池线程上执行,因此第二行为 True。这解释了为什么异步代码里不能依赖“回到发起线程”。
关键点: 没有同步上下文时,await 之后的线程不保证是原线程。
0655 关于 ValueTask<T> 与 Task<T>,哪项正确?
难度: 进阶
- A. 两者完全等价
- B. ValueTask 内部永远是 Task
- C. ValueTask 一定比 Task 慢
- D. ValueTask 旨在减少分配;同一实例不应被多次 await(契约上只能消费一次),Task 可以重复 await
查看答案与解析
正确答案D
解析: ValueTask 是为了在“结果立即可用”时省掉 Task 分配(同步完成时直接装值);但它的消费契约是“单次”:同一个 ValueTask 实例只应 await 一次(不能并发 await、不能存起来复用),而 Task 可任意多次 await。异步方法返回 ValueTask 时应确保调用方只消费一次。
关键点: ValueTask 省分配的前提是“一次性消费”。
0656 以下代码输出什么?
难度: 实战
static async Task M()
{
Thread.Sleep(100);
await Task.Delay(1);
}
var sw = Stopwatch.StartNew();
await M();
Console.WriteLine(sw.ElapsedMilliseconds >= 100);
- A.
False,async 方法不会阻塞调用线程 - B.
True,async 方法在第一个未完成 await 前同步执行,Thread.Sleep 阻塞了调用线程 - C. 抛
InvalidOperationException - D. 编译失败
查看答案与解析
正确答案B
解析: async 方法体在到达第一个未完成 await 之前是“同步执行”的,期间的 Thread.Sleep(100) 实打实阻塞调用线程;await M() 返回前调用线程已经被睡掉 100ms。把耗时逻辑放在第一个 await 之前并不会“异步化”。
关键点: async 不改变同步段;只有 await 之后才可能切线程。
0657 以下代码输出什么?
难度: 进阶
static async Task<int> Inner()
{
await Task.Delay(1);
return 42;
}
Task t = Task.Factory.StartNew(Inner);
Console.WriteLine(t is Task<Task<int>>);
- A.
True,StartNew 返回Task<Task<int>>,不展开,需要 Unwrap - B.
False - C. 编译失败
- D. 抛
InvalidOperationException
查看答案与解析
正确答案A
解析: Task.Factory.StartNew(Func<Task<int>>) 不识别内层任务,返回 Task<Task<int>>,await 它只会得到内层 Task 而不是 42;需要手动 Unwrap() 或用 Task.Run。
关键点: StartNew + async lambda = 双重任务,必须 Unwrap。
0658 以下代码输出什么?
难度: 实战
Task t = null!;
try { await t; }
catch (Exception e) { Console.WriteLine(e.GetType().Name); }
- A.
TaskCanceledException - B.
NullReferenceException,对 null 任务调用 GetAwaiter - C.
InvalidOperationException - D. 编译失败
查看答案与解析
正确答案B
解析: await t 编译成 t.GetAwaiter();t 为 null 时在 GetAwaiter 上抛 NullReferenceException。await 本身不检查 null,它只负责展开 awaiter。写通用扩展方法时先判空再 await。
关键点: await null 是运行时 NRE,不是编译期错误。
0659 以下代码输出什么?
难度: 进阶
Task<int> t = Task.FromResult(42);
Console.WriteLine(await t);
Console.WriteLine(await t);
- A.
42/ 抛异常 - B. 抛异常
- C. 编译失败
- D.
42/42,Task 可以多次 await
查看答案与解析
正确答案D
解析: Task 是“结果可重复读取”的对象,await 多少次都返回相同结果,不会消费掉。与 ValueTask 的单次消费契约形成对比。
关键点: Task 可复用,ValueTask 不可复用。
0660 哪种场景使用 async void 是可接受的?
难度: 实战
- A. 事件处理器(如 UI 按钮点击事件)
- B. 库的对外公开 API
- C. 返回值的工厂方法
- D. 构造函数
查看答案与解析
正确答案A
解析: async void 的异常无法被调用方捕获,唯一的合理场景是事件处理器(事件签名不允许返回 Task)。库 API、工厂、构造函数一律用 async Task/Task<T>,让调用方能观察异常与完成状态。
关键点: async void 是事件处理器的专用逃生口,不是通用返回类型。