以仁待人 發表於 2025-12-28 10:28:13

C#中Task.WhenAll和Task.WhenAny的使用与区别小结

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、先给终极结论</a></li><li><a href="#_label1">二、两者的本质模型(抽象层)</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_0">1、 Task.WhenAll &mdash;&mdash; &ldquo;全部完成门闩(AND Gate)&rdquo;</a></li><li><a href="#_lab2_1_1">2、 Task.WhenAny &mdash;&mdash; &ldquo;第一个完成门闩(OR Gate)&rdquo;</a></li></ul><li><a href="#_label2">三、底层实现原理(核心机制)</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_2">1、WhenAll 的内部机制(简化版)</a></li><li><a href="#_lab2_2_3">2、WhenAny 的内部机制(简化版)</a></li><li><a href="#_lab2_2_4">3、为什么 WhenAny 返回Task&lt;Task&gt;?</a></li></ul><li><a href="#_label3">四、执行与调度层面的关键差异</a></li><ul class="second_class_ul"></ul><li><a href="#_label4">五、异常语义(非常重要)</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_5">1、 WhenAll 的异常规则</a></li><li><a href="#_lab2_4_6">2、 WhenAny 的异常规则</a></li></ul><li><a href="#_label5">六、取消语义(常被误解)</a></li><ul class="second_class_ul"><li><a href="#_lab2_5_7">1、WhenAll</a></li><li><a href="#_lab2_5_8">2、 WhenAny</a></li></ul><li><a href="#_label6">七、性能特征(底层视角)</a></li><ul class="second_class_ul"><li><a href="#_lab2_6_9">1、 WhenAll / WhenAny 本身的成本</a></li><li><a href="#_lab2_6_10">2、真正的性能瓶颈来自:</a></li></ul><li><a href="#_label7">八、典型使用范式(工程级)</a></li><ul class="second_class_ul"><li><a href="#_lab2_7_11">1、 WhenAll &mdash;&mdash; 并发聚合(最常用)</a></li><li><a href="#_lab2_7_12">2、 WhenAny &mdash;&mdash; 竞速 / 超时 / 降级</a></li></ul><li><a href="#_label8">九、常见错误总结</a></li><ul class="second_class_ul"></ul><li><a href="#_label9">十、设计层面的黄金准则</a></li><ul class="second_class_ul"></ul><li><a href="#_label10">十一、一句话终极总结</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一、先给终极结论</h2>
<p>Task.WhenAll 和 Task.WhenAny 都不是执行器,而是&ldquo;完成信号的组合器&rdquo;。<br />它们:</p>
<ul><li>❌ 不创建线程</li><li>❌ 不调度任务</li><li>❌ 不产生并发</li><li>✅ 只监听 Task 的完成</li></ul>
<p><strong>并发发生在 Task 被&ldquo;创建/启动&rdquo;的那一刻,而不是 WhenAll/WhenAny。</strong></p>
<p class="maodian"><a name="_label1"></a></p><h2>二、两者的本质模型(抽象层)</h2>
<p class="maodian"><a name="_lab2_1_0"></a></p><h3>1、 Task.WhenAll &mdash;&mdash; &ldquo;全部完成门闩(AND Gate)&rdquo;</h3>
<div class="jb51code"><pre class="brush:plain;">T1 ─┐
T2 ─┼─▶ [ All Completed ] ─▶ Completed Task
T3 ─┘
</pre></div>
<p><strong>语义:</strong></p>
<ul><li>所有 Task 完成 &rarr; WhenAll 完成</li><li>任一 Task 失败 &rarr; WhenAll 失败(但仍等所有结束)</li></ul>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>2、 Task.WhenAny &mdash;&mdash; &ldquo;第一个完成门闩(OR Gate)&rdquo;</h3>
<div class="jb51code"><pre class="brush:plain;">T1 ─┐
T2 ─┼─▶ [ First Completed ] ─▶ Completed Task&lt;Task&gt;
T3 ─┘
</pre></div>
<p><strong>语义:</strong></p>
<ul><li>第一个完成者胜出</li><li>其他 Task 不受影响,继续运行</li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>三、底层实现原理(核心机制)</h2>
<p class="maodian"><a name="_lab2_2_2"></a></p><h3>1、WhenAll 的内部机制(简化版)</h3>
<ul><li><p>对每个 Task 注册 <strong>continuation</strong></p></li><li><p>使用一个 <strong>原子计数器</strong>(remaining)</p></li><li><p>每完成一个 Task:</p>
<ul><li>记录状态(成功 / 失败 / 取消)</li><li><code>Interlocked.Decrement(remaining)</code></li></ul></li><li><p>当 <code>remaining == 0</code>:</p>
<ul><li>设置 WhenAll Task 的最终状态</li></ul></li></ul>
<p>📌 <strong>没有线程等待,完全事件驱动</strong></p>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>2、WhenAny 的内部机制(简化版)</h3>
<p>对每个 Task 注册 continuation</p>
<p>第一个完成的 Task:</p>
<ul><li>调用 <code>TrySetResult(task)</code></li></ul>
<p>后续完成者:</p>
<ul><li>直接忽略</li></ul>
<p>📌 <strong>只有一个能&ldquo;赢&rdquo;,没有计数器</strong></p>
<p class="maodian"><a name="_lab2_2_4"></a></p><h3>3、为什么 WhenAny 返回Task&lt;Task&gt;?</h3>
<ul><li>外层 Task:表示&ldquo;谁先完成&rdquo;</li><li>内层 Task:表示&ldquo;完成的那个任务本身&rdquo;</li></ul>
<blockquote><p>WhenAny 返回的是&ldquo;胜者句柄&rdquo;,不是结果</p></blockquote>
<p class="maodian"><a name="_label3"></a></p><h2>四、执行与调度层面的关键差异</h2>
<table><thead><tr><th>维度</th><th>WhenAll</th><th>WhenAny</th></tr></thead><tbody><tr><td>等待策略</td><td>全部</td><td>第一个</td></tr><tr><td>返回类型</td><td>Task / Task&lt;T[]&gt;</td><td>Task</td></tr><tr><td>是否阻塞线程</td><td>❌</td><td>❌</td></tr><tr><td>continuation 数量</td><td>N</td><td>N</td></tr><tr><td>内部同步</td><td>原子计数</td><td>CAS / TrySet</td></tr><tr><td>并发控制</td><td>❌</td><td>❌</td></tr></tbody></table>
<blockquote><p>二者都只做&ldquo;信号组合&rdquo;,不做&ldquo;任务调度&rdquo;</p></blockquote>
<p class="maodian"><a name="_label4"></a></p><h2>五、异常语义(非常重要)</h2>
<p class="maodian"><a name="_lab2_4_5"></a></p><h3>1、 WhenAll 的异常规则</h3>
<ul><li><p><strong>所有 Task 都会执行到结束</strong></p></li><li><p>如果有异常:</p>
<ul><li><p>内部保存 <code>AggregateException</code></p></li><li><p><code>await WhenAll</code> 时:</p>
<ul><li>抛出 <strong>第一个异常</strong></li><li>其他异常仍可从各 Task 中取</li></ul></li></ul></li></ul>
<div class="jb51code"><pre class="brush:csharp;">try
{
    await Task.WhenAll(tasks);
}
catch
{
    var all = tasks
      .Where(t =&gt; t.IsFaulted)
      .SelectMany(t =&gt; t.Exception!.InnerExceptions);
}
</pre></div>
<p class="maodian"><a name="_lab2_4_6"></a></p><h3>2、 WhenAny 的异常规则</h3>
<ul><li><p>如果&ldquo;第一个完成的 Task&rdquo;失败:</p>
<ul><li><code>await completedTask</code> 会直接抛异常</li></ul></li><li><p>其他 Task 的异常:</p>
<ul><li><strong>不会被观察</strong></li><li>必须手动处理,否则可能变成未观察异常</li></ul></li></ul>
<p>📌 <strong>WhenAny + 异常 = 高风险组合</strong></p>
<p class="maodian"><a name="_label5"></a></p><h2>六、取消语义(常被误解)</h2>
<p class="maodian"><a name="_lab2_5_7"></a></p><h3>1、WhenAll</h3>
<ul><li><p>如果任一 Task 被取消:</p>
<ul><li>WhenAll <strong>最终可能是 Canceled</strong></li></ul></li><li><p>但:</p>
<ul><li><strong>不会主动取消其他 Task</strong></li></ul></li></ul>
<p class="maodian"><a name="_lab2_5_8"></a></p><h3>2、 WhenAny</h3>
<ul><li>不会取消任何 Task</li><li>取消必须由你显式触发</li></ul>
<div class="jb51code"><pre class="brush:csharp;">var winner = await Task.WhenAny(tasks);
cts.Cancel(); // 你自己的责任
</pre></div>
<p class="maodian"><a name="_label6"></a></p><h2>七、性能特征(底层视角)</h2>
<p class="maodian"><a name="_lab2_6_9"></a></p><h3>1、 WhenAll / WhenAny 本身的成本</h3>
<ul><li>少量对象分配</li><li>N 个 continuation</li><li>原子操作 / CAS</li></ul>
<p>👉 <strong>几乎可以忽略</strong></p>
<p class="maodian"><a name="_lab2_6_10"></a></p><h3>2、真正的性能瓶颈来自:</h3>
<ul><li>Task 创建方式</li><li>IO / CPU 本身</li><li>Task.Run / ThreadPool 使用</li><li>并发规模失控</li></ul>
<p class="maodian"><a name="_label7"></a></p><h2>八、典型使用范式(工程级)</h2>
<p class="maodian"><a name="_lab2_7_11"></a></p><h3>1、 WhenAll &mdash;&mdash; 并发聚合(最常用)</h3>
<div class="jb51code"><pre class="brush:csharp;">var t1 = GetUserAsync();
var t2 = GetOrdersAsync();

await Task.WhenAll(t1, t2);

return new
{
    User = await t1,
    Orders = await t2
};
</pre></div>
<p>适用场景</p>
<ul><li>多个独立 IO</li><li>聚合响应</li><li>Web API</li></ul>
<p class="maodian"><a name="_lab2_7_12"></a></p><h3>2、 WhenAny &mdash;&mdash; 竞速 / 超时 / 降级</h3>
<p>超时模式</p>
<div class="jb51code"><pre class="brush:csharp;">var work = DoWorkAsync();
var timeout = Task.Delay(2000);

if (await Task.WhenAny(work, timeout) == timeout)
    throw new TimeoutException();

await work;
</pre></div>
<p>主备切换</p>
<div class="jb51code"><pre class="brush:csharp;">var tasks = new[]
{
    CallPrimaryAsync(),
    CallSecondaryAsync()
};

var winner = await Task.WhenAny(tasks);
return await winner;
</pre></div>
<p>⚠️ 记得取消失败者</p>
<p class="maodian"><a name="_label8"></a></p><h2>九、常见错误总结</h2>
<p>❌ 把 WhenAll 当&ldquo;并行器&rdquo;<br />❌ 在循环里直接 await(伪并发)<br />❌ WhenAny 后忽略未完成 Task<br />❌ WhenAll + Task.Run(Web)<br />❌ 忽略异常和取消</p>
<p class="maodian"><a name="_label9"></a></p><h2>十、设计层面的黄金准则</h2>
<blockquote><p>并发 = Task 创建时机<br />组合 = WhenAll / WhenAny<br />调度 = ThreadPool / Parallel</p></blockquote>
<blockquote><p>WhenAll / WhenAny 决定&ldquo;怎么等&rdquo;,<br />而不是&ldquo;怎么跑&rdquo;。</p></blockquote>
<p class="maodian"><a name="_label10"></a></p><h2>十一、一句话终极总结</h2>
<blockquote><p>Task.WhenAll 是一个原子计数器驱动的完成门闩<br />Task.WhenAny 是一个 CAS 驱动的竞速门闩</p></blockquote>
<blockquote><p>它们本身几乎&ldquo;没有重量&rdquo;,<br />但决定了整个 async 架构的形状。</p></blockquote>
頁: [1]
查看完整版本: C#中Task.WhenAll和Task.WhenAny的使用与区别小结