自定义HttpClient场景下,在已await的异步方法上调用.Result是否可行?
.Result是否可行? 结论:绝对不推荐这么做,大概率会导致死锁,还存在其他线程安全和资源浪费问题。
核心问题:死锁风险
在有同步上下文的环境(比如传统ASP.NET、WPF、WinForms)中,调用Task.Result会阻塞当前线程,而异步方法GetTokenAndRefreshRate里的await会试图回到原来的同步上下文继续执行。这时候被阻塞的线程占着同步上下文,异步方法无法获取上下文继续运行,就会形成死锁,导致程序卡死。
即使在没有同步上下文的环境(比如控制台程序、ASP.NET Core)里不会死锁,这种写法也是不良实践——阻塞线程会浪费线程池资源,违背异步编程的初衷。
你代码里的其他问题
静态变量线程不安全
_token、_refreshRate、_tokenExpires都是静态字段,多个线程同时调用CustomClient.Client时,会出现竞态条件:比如多个线程同时判断令牌过期,然后重复调用令牌接口,甚至导致变量值被错误覆盖。频繁创建HttpClient浪费资源
HttpClient是设计为可复用的实例,每次调用GetNewClient都新建HttpClient会快速耗尽系统Socket资源,导致后续请求失败。异步方法没必要的封装
GetTokenAndRefreshRate里用Task.Run包裹同步方法TokenManager.CreateAuthorizationToken,然后又await这个任务,完全是多此一举——同步方法没必要强行包装成异步,反而浪费线程池线程。
修复方案
1. 改用异步方式获取客户端,避免.Result
把获取客户端的逻辑改成异步,用await替代Result:
public class CustomClient { private static int _refreshRate = 0; private static string _token; private static DateTime _tokenExpires = DateTime.Now.AddDays(-1); private static readonly HttpClient _httpClient; private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); static CustomClient() { var clientHandler = new HttpClientHandler(); clientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; _httpClient = new HttpClient(clientHandler); } public static async Task<HttpClient> GetClientAsync() { await _semaphore.WaitAsync(); try { var now = DateTime.Now; if (now >= _tokenExpires) { var (token, refreshRate) = await GetTokenAndRefreshRateAsync(); _token = token; _refreshRate = refreshRate; _tokenExpires = now.AddSeconds(refreshRate); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token); } if (string.IsNullOrEmpty(_token)) { throw new Exception("无法获取访问令牌"); } return _httpClient; } finally { _semaphore.Release(); } } // 如果TokenManager.CreateAuthorizationToken是同步方法,直接同步调用即可 private static Task<(string Token, int RefreshRate)> GetTokenAndRefreshRateAsync() { int refreshRate = 0; string token = TokenManager.CreateAuthorizationToken(out refreshRate); return Task.FromResult((token, refreshRate)); } }
关键优化点:
- 用
SemaphoreSlim保证静态变量的线程安全,避免多线程竞态条件。 - 静态构造函数中只创建一次HttpClient,复用实例节省资源。
- 把异步逻辑改成真正的异步调用,去掉不必要的
Task.Run。 - 用C#元组替代
Tuple,代码更简洁。
总结
永远不要在同步方法中调用异步方法的.Result或.Wait(),这是异步编程的典型反模式。正确的做法是让调用链全程异步,同时注意线程安全和资源复用。
内容的提问来源于stack exchange,提问作者John Grove

