C# 试题 37:并行、PLINQ 与 Channel
0721 关于并行化适用性,哪项正确?
难度: 进阶
- A. 任何 for 循环改用 Parallel.For 都会更快
- B. 并行化适合足够大、CPU 密集且可分割的工作;细粒度任务会被调度开销抵消,I/O 等待应优先使用异步而非占线程并行
- C. Parallel.For 保证按索引顺序执行
- D. Parallel.For 只适用于数组
查看答案与解析
正确答案B
解析: 并行化收益来自多个核心同时做 CPU 密集工作;任务太小则分区、调度、同步的开销超过收益,I/O 等待场景则浪费线程资源,应使用异步。Parallel 系列从不承诺执行顺序。
关键点: 并行适合大的 CPU 密集可分割工作,I/O 走异步。
0722 以下代码的结果是什么?
难度: 实战
int sum = 0;
Parallel.For(0, 10_000, i =>
{
sum += i;
});
Console.WriteLine(sum);
- A. 总是输出
49995000 - B. 抛 AggregateException
- C. 结果不确定:sum += i 是读-改-写,多个迭代并发执行存在数据竞争,通常小于 49995000
- D. 编译失败
查看答案与解析
正确答案C
解析: sum += i 不是原子操作,并行迭代会交错读写,丢失更新。正确做法是各迭代用局部变量累加后再合并,或使用 Interlocked(此处 int 足够,注意期望和未溢出)。结果不能作为确定值依赖。
关键点: Parallel.For 体内共享可变状态是数据竞争,需局部聚合。
0723 以下代码哪项正确?
难度: 进阶
Parallel.For(0, 10, i =>
{
Console.Write(i + " ");
});
- A. 输出顺序不确定,但每个索引恰好执行一次;Parallel.For 不保证迭代顺序
- B. 一定输出
0 1 2 3 4 5 6 7 8 9 - C. 一定输出倒序
- D. 部分索引可能被跳过
查看答案与解析
正确答案A
解析: Parallel.For 把索引区间分区交给多个线程并发执行,因此输出顺序不定;但每个索引都会且只会执行一次(除非异常提前终止)。依赖顺序的代码不应使用 Parallel 系列。
关键点: Parallel.For 无顺序保证,但有完整性保证。
0724 以下代码输出什么?
难度: 实战
try
{
Parallel.For(0, 10, i =>
{
if (i == 3) throw new InvalidOperationException("boom");
});
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name);
}
- A.
InvalidOperationException - B. 什么都不输出
- C.
Exception - D.
AggregateException:Parallel.For 把并行迭代中抛出的异常聚合后重新抛出
查看答案与解析
正确答案D
解析: 并行循环可能同时有多个迭代抛异常,因此 Parallel.For 把异常包装进 AggregateException 统一抛出。不要期望直接捕获原始异常类型。
关键点: Parallel 系列的异常以 AggregateException 形式出现。
0725 以下代码哪项正确?
难度: 实战
var items = Enumerable.Range(0, 10).ToList();
await Parallel.ForEachAsync(items, async (item, token) =>
{
await Task.Delay(10, token);
Console.Write(item + " ");
});
- A. 输出严格按 0 到 9 顺序
- B. 输出顺序不确定,但每个元素都会被处理一次;ForEachAsync 不保证处理顺序
- C. 编译失败:body 不能是 async 方法
- D. 一定抛 AggregateException
查看答案与解析
正确答案B
解析: Parallel.ForEachAsync(.NET 6+)的 body 是 Func<T, CancellationToken, ValueTask>,支持 async;元素按分区并发处理,顺序不保证,但每个元素处理一次。它返回 Task,可 await。
关键点: ForEachAsync 支持异步 body,顺序不保证。
0726 以下代码哪项正确?
难度: 进阶
await Parallel.ForEachAsync(
Enumerable.Range(0, 100),
new ParallelOptions { MaxDegreeOfParallelism = 4 },
async (i, ct) => await Task.Delay(10, ct));
- A. 同一时刻最多约 4 个 body 并发执行;MaxDegreeOfParallelism 是并发上限,运行时可能执行更少
- B. 100 个 body 同时执行
- C. 严格按顺序逐个执行
- D. 编译失败:ForEachAsync 没有带 ParallelOptions 的重载
查看答案与解析
正确答案A
解析: MaxDegreeOfParallelism 是并发执行的 body 数量上限,运行时在资源不足或任务提前完成时可能低于该值,但不会超过。默认并发度与处理器数量相关,不适合所有场景。
关键点: MaxDegreeOfParallelism 是上限而非目标值。
0727 以下代码输出什么?
难度: 实战
try
{
await Parallel.ForEachAsync(Enumerable.Range(0, 5), async (i, ct) =>
{
if (i == 2) throw new InvalidOperationException("x");
await Task.Yield();
});
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name);
}
- A.
InvalidOperationException - B.
TaskCanceledException - C.
AggregateException:ForEachAsync 把 body 抛出的异常聚合到返回的 Task 中 - D. 什么都不输出
查看答案与解析
正确答案C
解析: ForEachAsync 内部收集各并发 body 的异常,合并为 AggregateException 让返回的 Task 失败;await 时抛出的是 AggregateException,需要解包 InnerExceptions。
关键点: ForEachAsync 与 Parallel.For 一样聚合异常。
0728 处理大量 I/O 等待型任务(如逐个请求 HTTP 接口),哪项选型更合理?
难度: 进阶
- A. 用 Parallel.For + Thread.Sleep 阻塞等待
- B. Parallel.ForEachAsync 配合真正的异步等待,I/O 等待期间不占用线程;也可用 Channel 限流,两者都优于阻塞线程的并行
- C. Parallel.ForEachAsync 只适合 CPU 密集任务
- D. 必须用 Task.Run 包住每个请求
查看答案与解析
正确答案B
解析: I/O 密集任务等待的是外部响应,占住线程没有收益。ForEachAsync 的 body 内 await 真正的异步 I/O 时线程被释放,配合 MaxDegreeOfParallelism 即可限流;生产级限流也可用 Channel + 固定数量消费者。
关键点: I/O 密集用异步并发而非阻塞并行。
0729 以下代码哪项正确?
难度: 进阶
var result = Enumerable.Range(0, 10).AsParallel()
.Select(x => x * 2)
.ToList();
- A. 一定按 0,2,4,…,18 排列
- B. 一定乱序
- C. 编译失败:AsParallel 不能与 Select 连用
- D. 顺序不确定:AsParallel 默认不保证保持源顺序,需要 AsOrdered 才保证有序输出
查看答案与解析
正确答案D
解析: PLINQ 默认以分区并行执行并在需要时按效率合并输出,不承诺源顺序。若下游依赖顺序,必须调用 AsOrdered()(以合并开销为代价)或在最终阶段排序。
关键点: AsParallel 默认无顺序保证,AsOrdered 才有。
0730 以下代码输出什么?
难度: 实战
var result = Enumerable.Range(0, 10).AsParallel().AsOrdered()
.Select(x => x * 2)
.ToList();
Console.WriteLine(string.Join(",", result));
- A.
0,2,4,6,8,10,12,14,16,18:AsOrdered 保持源顺序(以合并开销为代价) - B. 顺序随机
- C. 编译失败:AsOrdered 与 AsParallel 不能连用
- D. 抛 InvalidOperationException
查看答案与解析
正确答案A
解析: AsOrdered() 让 PLINQ 在保留并行执行的同时按源顺序输出结果,代价是结果合并阶段需要缓冲与排序。因此输出为源序列各元素乘 2 的有序列表。
关键点: AsOrdered 换顺序保证,付出合并开销。
0731 以下代码哪项正确?
难度: 实战
Enumerable.Range(0, 10).AsParallel().ForAll(x =>
{
Console.Write(x + " ");
});
- A. 一定输出 0 到 9 顺序
- B. 输出顺序不确定:ForAll 跳过有序合并,直接并行执行副作用
- C. 编译失败:ForAll 只接受 Action
- D. 输出一定为空
查看答案与解析
正确答案B
解析: ForAll 不产生有序结果,直接在分区线程上执行委托,省略了合并排序,是“只做副作用”场景的优化选择。副作用本身要保证线程安全,顺序不可依赖。
关键点: ForAll 跳过有序合并,顺序无保证。
0732 以下代码输出什么?
难度: 实战
try
{
Enumerable.Range(0, 10).AsParallel().Select(x =>
{
if (x == 4) throw new Exception("boom");
return x;
}).ToList();
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name);
}
- A.
Exception - B.
InvalidOperationException - C.
AggregateException:PLINQ 把并行查询中的异常聚合抛出 - D. 不输出:查询是惰性的,ToList 前不执行
查看答案与解析
正确答案C
解析: 查询在 ToList() 时被枚举执行,PLINQ 把各分区抛出的异常收集并包装为 AggregateException。注意异常在枚举(执行)时抛出,而不是在构造查询时。
关键点: PLINQ 异常聚合为 AggregateException,且发生在执行时。
0733 以下代码输出什么?
难度: 实战
var channel = Channel.CreateBounded<int>(3);
for (int i = 0; i < 3; i++)
channel.Writer.TryWrite(i);
bool ok = channel.Writer.TryWrite(3);
Console.WriteLine(ok);
- A.
True - B.
False:容量已满,TryWrite 不阻塞,满时立即返回 false(背压信号) - C. 抛 ChannelClosedException
- D. 阻塞直到有空间
查看答案与解析
正确答案B
解析: TryWrite 是“尽力而为”的同步写入:容量满时立即返回 false,绝不阻塞。有界通道的背压主要靠满时 WriteAsync 挂起或 TryWrite 返回 false 体现。
关键点: TryWrite 满时返回 false,是背压的同步信号。
0734 以下代码输出什么?
难度: 进阶
var channel = Channel.CreateBounded<int>(1);
await channel.Writer.WriteAsync(1); // 立即完成
var pending = channel.Writer.WriteAsync(2); // 容量满,等待消费者
Console.WriteLine(pending.IsCompleted);
Console.WriteLine(await channel.Reader.ReadAsync());
Console.WriteLine(await pending);
- A.
True - B.
False\n2\n1 - C. 抛 ChannelClosedException
- D.
False\n1\n2:有界通道满时 WriteAsync 挂起形成背压,消费者读走一个后写入完成
查看答案与解析
正确答案D
解析: 有界通道默认 FullMode.Wait:容量满时 WriteAsync 返回未完成的 ValueTask,消费者读取后腾出空间,该写入才完成。因此先输出 False,再读到 1,最后 pending 完成并返回 2。
关键点: 有界通道满时 WriteAsync 挂起,读取即背压释放。
0735 以下代码输出什么?
难度: 进阶
var channel = Channel.CreateBounded<int>(2, new BoundedChannelOptions(2)
{
FullMode = BoundedChannelFullMode.DropOldest
});
channel.Writer.TryWrite(1);
channel.Writer.TryWrite(2);
channel.Writer.TryWrite(3);
Console.WriteLine(await channel.Reader.ReadAsync());
Console.WriteLine(await channel.Reader.ReadAsync());
- A.
2\n3:DropOldest 在容量满时丢弃最旧项,1 被挤出 - B.
1\n2 - C.
3\n3 - D. 抛 ChannelClosedException
查看答案与解析
正确答案A
解析: DropOldest 模式下,容量满时新写入会丢弃最旧的元素:写入 1、2 后容量满,写 3 时 1 被挤出,剩余 2、3。首次读得 2,再读得 3。
关键点: DropOldest 丢弃最旧项,其余写入模式还有 DropNewest/DropWrite。
0736 关于 Channel 的背压,哪项正确?
难度: 进阶
- A. 无界通道自带背压
- B. 有界通道与无界通道在背压上完全等价
- C. 有界通道通过容量上限提供背压:满时 WriteAsync 挂起(默认 FullMode.Wait),无界通道写入永不阻塞,可能耗尽内存
- D. 有界通道不能用于并发生产消费
查看答案与解析
正确答案C
解析: 无界通道的 WriteAsync/TryWrite 永不因容量阻塞,生产速度超过消费速度时队列无限增长直至 OOM;有界通道以容量为界,满时挂起写入者,把背压传导给生产者,这是流式管道的正确姿势。
关键点: 背压来自有界容量,无界通道可能内存失控。
0737 以下代码输出什么?
难度: 实战
var channel = Channel.CreateUnbounded<int>();
channel.Writer.TryComplete();
bool second = channel.Writer.TryComplete();
Console.WriteLine(second);
- A.
True - B.
False:通道已完成后,再次 TryComplete 返回 false,不抛异常 - C. 抛 InvalidOperationException
- D.
True,并重置通道
查看答案与解析
正确答案B
解析: TryComplete 在通道已处于完成状态时返回 false,不抛异常,适合“多个生产者各自尝试完成”的竞态场景。真正要抛异常提示重复完成,才用 Complete()。
关键点: TryComplete 幂等安全,重复调用返回 false。
0738 以下代码的结果是什么?
难度: 实战
var channel = Channel.CreateUnbounded<int>();
channel.Writer.Complete();
channel.Writer.Complete();
- A. 正常运行
- B. 抛 ChannelClosedException
- C. 第二次调用被忽略
- D. 第二次 Complete() 抛 InvalidOperationException:通道已完成后 Complete 抛异常,应使用 TryComplete
查看答案与解析
正确答案D
解析: Complete() 在通道已完成后第二次调用会抛 InvalidOperationException;TryComplete() 则返回 false。多生产者场景应统一使用 TryComplete 避免竞态异常。
关键点: Complete 重复调用抛异常,TryComplete 才是安全版本。
0739 以下代码输出什么?
难度: 实战
var channel = Channel.CreateUnbounded<int>();
channel.Writer.TryComplete();
bool ok = channel.Writer.TryWrite(1);
Console.WriteLine(ok);
- A.
False:通道完成后 TryWrite 返回 false - B.
True - C. 抛 ChannelClosedException
- D. 抛 InvalidOperationException
查看答案与解析
正确答案A
解析: 通道完成后写入被拒绝:TryWrite 返回 false 而不抛异常;WriteAsync 则会抛 ChannelClosedException。生产者在收到 false 时通常应立即停止生产。
关键点: 完成后的写入:TryWrite 返回 false,WriteAsync 抛异常。
0740 以下代码输出什么?
难度: 实战
var channel = Channel.CreateUnbounded<int>();
await channel.Writer.WriteAsync(1);
channel.Writer.TryComplete();
var items = new List<int>();
await foreach (var item in channel.Reader.ReadAllAsync())
items.Add(item);
Console.WriteLine(items.Count);
Console.WriteLine(await channel.Reader.WaitToReadAsync());
- A.
1\nTrue - B.
1\nFalse:ReadAllAsync 读到完成结束;完成且清空后 WaitToReadAsync 返回 false - C.
1\n1 - D. 抛 ChannelClosedException
查看答案与解析
正确答案B
解析: ReadAllAsync 在通道完成且缓冲清空后正常结束。之后再调用 WaitToReadAsync:通道已完成且无剩余数据,返回 false,表示读取方可以退出。这是“观察完成状态”的可靠方式。
关键点: WaitToReadAsync 返回 false 表示完成且无数据,读取方据此退出。