怀石逾沙 發表於 2025-11-6 09:50:37

.NET中异步防超时的3种硬核方法与避坑指南

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、CancellationToken:异步的&ldquo;刹车片&rdquo;</a></li><li><a href="#_label1">二、HttpClient.Timeout:让请求&ldquo;有底线&rdquo;(别被坑了还蒙在鼓里)</a></li><li><a href="#_label2">三、自定义重试策略:超时后的&ldquo;翻盘&rdquo;(不是所有超时都该放弃)</a></li><li><a href="#_label3">5个超时陷阱:踩中一个,半夜被叫醒</a></li><li><a href="#_label4">尾声:超时不是问题,是你的护城河</a></li></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一、CancellationToken:异步的&ldquo;刹车片&rdquo;</h2>
<div class="jb51code"><pre class="brush:csharp;">// 重点来了!别让异步请求变成“幽灵线程”——
// 这行是灵魂!没有它,超时就是个摆设
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // 10秒超时,不是10分钟!
// 为啥不是10分钟?因为用户等不起,产品经理更等不起!
</pre></div>
<div class="jb51code"><pre class="brush:csharp;">// 模拟一个网络请求(比如调第三方API)
var httpClient = new HttpClient();
try
{
    // 关键:把CancellationToken传进去,这才是“防超时”的核心
    var response = await httpClient.GetStringAsync("https://api.example.com/data", cts.Token); // 传入令牌!
    // 如果超时,这里会抛OperationCanceledException
}
catch (TaskCanceledException ex)
{
    // 你猜怎么着?这异常才是你该处理的!
    // 别直接log,得优雅降级:比如返回缓存或默认值
    LogWarning($"API请求超时:{ex.Message},已切换到本地缓存");
    return GetLocalFallbackData(); // 优雅降级,别让用户看到500
}
finally
{
    // 一定要释放资源!别让CancellationTokenSource变成内存幽灵
    cts.Dispose(); // 释放,别留垃圾!
}
</pre></div>
<p><strong>注释:</strong></p>
<ul><li>这行代码写错,等于给服务器埋了个定时炸弹。</li><li>超时时间别乱设&mdash;&mdash;10秒是底线,别想着&ldquo;再等5秒&rdquo;,用户不会等,产品经理更不会!</li><li>CancellationTokenSource不Dispose?内存泄漏?那是你下辈子的简历。</li></ul>
<p class="maodian"><a name="_label1"></a></p><h2>二、HttpClient.Timeout:让请求&ldquo;有底线&rdquo;(别被坑了还蒙在鼓里)</h2>
<div class="jb51code"><pre class="brush:csharp;">// 重点来了!别以为HttpClient默认超时是30秒——
// 实际上,它默认是0(永不超时)!
var httpClient = new HttpClient
{
    // 这行是保命符!没它,超时等于没超时
    Timeout = TimeSpan.FromSeconds(10) // 10秒,不是10分钟!
};
</pre></div>
<div class="jb51code"><pre class="brush:csharp;">// 调用示例
try
{
    var response = await httpClient.GetStringAsync("https://api.example.com/data");
    // 如果超时,这里会抛TaskCanceledException
}
catch (HttpRequestException ex) when (ex.InnerException is TaskCanceledException)
{
    // 别懵!这是超时异常,不是网络故障
    LogError($"第三方API超时:{ex.Message},已触发熔断机制");
    return HandleTimeoutFallback(); // 触发熔断,别硬扛
}
</pre></div>
<p><strong>注释:</strong></p>
<ul><li>HttpClient.Timeout和CancellationToken是&ldquo;双保险&rdquo;,不是&ldquo;单保险&rdquo;!</li><li>为啥要双保险?因为CancellationToken处理不了HttpClient本身的超时逻辑。</li><li>设置为0?你这是在给线上服务开&ldquo;无限期等死&rdquo;模式&mdash;&mdash;别问,问就是血泪史。</li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>三、自定义重试策略:超时后的&ldquo;翻盘&rdquo;(不是所有超时都该放弃)</h2>
<div class="jb51code"><pre class="brush:csharp;">// 重点来了!别一超时就直接报错——
// 有些超时是网络抖动,重试一次可能就回来了
var retryPolicy = Policy
    .Handle&lt;HttpRequestException&gt;(ex =&gt; ex.InnerException is TaskCanceledException)
    .WaitAndRetryAsync(3, retryAttempt =&gt;
      TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); // 指数退避:2秒、4秒、8秒
</pre></div>
<div class="jb51code"><pre class="brush:csharp;">// 使用重试策略
try
{
    var result = await retryPolicy.ExecuteAsync(
      () =&gt; httpClient.GetStringAsync("https://api.example.com/data"));
    return result;
}
catch (Exception ex)
{
    // 所有重试都失败了,才走这一步
    LogCritical($"API请求彻底失败:{ex.Message}");
    return FallbackToOfflineMode(); // 降级到离线模式
}
</pre></div>
<p><strong>注释:</strong></p>
<ul><li>重试不是万能的!别让超时变成&ldquo;重试地狱&rdquo;。</li><li>指数退避是精髓:第一次2秒,第二次4秒,第三次8秒&mdash;&mdash;别一上来就等10分钟。</li><li>重试3次?是上限,不是下限。超过3次,直接切降级,别浪费用户时间。</li></ul>
<p class="maodian"><a name="_label3"></a></p><h2>5个超时陷阱:踩中一个,半夜被叫醒</h2>
<p><strong>1.&ldquo;我设了超时,就不用管了&rdquo;</strong></p>
<p>陷阱:以为超时时间一设,就万事大吉。</p>
<p>现实:超时异常不处理,线程会堆积,服务直接崩。</p>
<p>墨工血泪:去年线上事故,就因为没处理超时异常,服务器内存爆了。</p>
<p><strong>2.&ldquo;用Task.Wait()代替await&rdquo;</strong></p>
<p>陷阱:<code>var result = httpClient.GetStringAsync(...).Wait();</code></p>
<p>现实:<code>Wait()</code>会阻塞主线程,超时了还卡住,比异步还坑。</p>
<p>墨工自嘲:当年我这么写,被老大骂&ldquo;你这是给异步加了个锁,还是给同步加了个异步?&rdquo;</p>
<p><strong>3.&ldquo;超时时间设得太长&rdquo;</strong></p>
<p>陷阱:<code>TimeSpan.FromSeconds(30)</code>,以为够长。</p>
<p>现实:用户等30秒?早退了。产品经理:你这超时设置得,比我的咖啡还慢。</p>
<p>墨工扎心:优化后,RT从30秒降到3秒,产品经理终于不半夜发&ldquo;在吗?&rdquo;了。</p>
<p><strong>4.&ldquo;CancellationTokenSource不Dispose&rdquo;</strong></p>
<p>陷阱:<code>var cts = new CancellationTokenSource();</code> 用完不Dispose。</p>
<p>现实:内存泄漏,GC都救不了。</p>
<p>墨工吐槽:这玩意儿不Dispose,比我的烟灰缸还容易爆。</p>
<p><strong>5.&ldquo;所有超时都一样处理&rdquo;</strong></p>
<p>陷阱:不管啥超时,都返回500错误。</p>
<p>现实:有些是网络抖动,重试就能好,直接返回500?用户要投诉。</p>
<p>墨工点睛:超时&ne;失败,超时&ne;用户要的。</p>
<p class="maodian"><a name="_label4"></a></p><h2>尾声:超时不是问题,是你的护城河</h2>
<p><strong>&ldquo;超时不是bug,是需求。&rdquo;</strong></p>
<p>这句话,我写了三年,才懂它真味。</p>
<p>你设的超时时间,决定了用户等多久;</p>
<p>你处理超时的方式,决定了用户走不走。</p>
<p><strong>3招:CancellationToken + HttpClient.Timeout + 自定义重试</strong>,</p>
<p><strong>5个坑:别当愣头青,别当&ldquo;超时终结者&rdquo;</strong>。</p>
頁: [1]
查看完整版本: .NET中异步防超时的3种硬核方法与避坑指南