作为资深的.NET 开发人员,我们都曾将 async/await
用作处理异步操作的常用模式。它简洁、直观,并且使我们的代码更易于维护。然而,在这种优秀的语法背后,隐藏着一套复杂的机制,一旦被误用,可能会对应用程序的性能产生重大影响。
本文将揭示其中隐藏的代价,并探讨每一位经验丰富的开发人员都应该了解的优化策略。
目录
理解基础原理 .NET 中的 async/await
模式从根本上改变了我们编写异步代码的方式。在学习高级模式之前,让我们先了解一下当我们编写异步代码时,底层会发生什么。
异步状态机流程… 幕后实际发生了什么? 当你将一个方法标记为 async
时,.NET 会进行一些有趣的操作。它会获取你的代码,并将其转换为一种特殊的结构,称为 “状态机”。
可以把它想象成将你的代码分解成更小的部分,这些部分可以暂停和恢复。
是的,你编写的代码是这样的:
public async Task<int> ProcessOrderAsync()
{
var data = await GetDataAsync(); // 步骤 1
var result = await ProcessDataAsync(data); // 步骤 2
return result;
}
但是,它实际变成的样子(简化后)是:
public Task<int>ProcessOrderAsync()
{
// 创建一个结构来跟踪我们当前的位置
var stateMachine =newAsyncStateMachine();
// 存储任何局部变量
stateMachine.data =null;
stateMachine.result =0;
// 开始处理
Start(stateMachine);
// 返回一个最终会包含结果的 Task
return stateMachine.Task;
}
为什么这很重要(对性能的影响) 这种转换会带来一些代价:
看看这个…
// 简单但可能会浪费资源
publicasyncTask<int>GetValueAsync()
{
returnawait Task.FromResult(42);
}
// 对于简单情况更高效
publicTask<int>GetValueBetter()
{
return Task.FromResult(42);
}
在第一个版本中,我们创建了一个实际上并不需要的状态机。第二个版本更高效,因为它直接返回了结果!
看到了吧!async/await
是根源所在,对吧?但要记住,它虽然重要,但并非在所有地方都有必要使用!我们可以进行优化…
让你的代码运行得更快:ValueTask
现在,让我们谈谈 ValueTask
。可以把它看作是在特定情况下比 Task
更高效的版本。以下是你可能想要使用它的情况:
是的,
// 之前:使用常规的 Task
public async Task<int> GetDataAsync(string key)
{
var value = await _database.GetValueAsync(key);
return value;
}
但是,
// 之后:高效地使用 ValueTask
publicValueTask<int>GetDataAsync(string key)
{
// 如果数据在缓存中,立即返回
if(_cache.TryGetValue(key,outvarvalue))
{
returnnewValueTask<int>(value);
}
// 如果不在缓存中,回退到异步操作
returnnewValueTask<int>(_database.GetValueAsync(key));
}
你应该在什么时候使用 ValueTask
呢?
📍不要仅仅因为 ValueTask
听起来更好就使用它。如果使用不当,它实际上会对性能产生更糟糕的影响!
时间到!
— 性能提示✓ 0️⃣ 不需要时不要使用异步
// 不要这样做
publicasyncTask<int>AddNumbers(int a,int b)
{
returnawait Task.FromResult(a + b);// 为什么要异步呢?
}
// 而是这样做
publicintAddNumbers(int a,int b)
{
return a + b;// 简单又快速!
}
1️⃣ 巧妙地处理多个操作
// 效率较低:一次处理一个
publicasyncTaskProcessItems(List<int> items)
{
foreach(var item in items)
{
awaitProcessItemAsync(item);// 一次处理一个
}
}
// 效率更高:一起处理
publicasyncTaskProcessItems(List<int> items)
{
var tasks = items.Select(item =>ProcessItemAsync(item));
await Task.WhenAll(tasks);// 一起处理!
}
2️⃣ 尽可能使用缓存
private readonlyDictionary<string, Task<int>> _cache =new();
publicasyncTask<int>GetExpensiveData(string key)
{
if(!_cache.TryGetValue(key,outvar task))
{
task =CalculateExpensiveDataAsync(key);
_cache[key]= task;
}
returnawait task;
}
3️⃣ 线程池及其重要性 线程池就像是一组随时准备处理你的异步操作的工作线程。有时你需要帮助它更好地工作:
public voidConfigureThreadPool()
{
// 获取当前设置
ThreadPool.GetMinThreads(outint workerThreads,outint completionPortThreads);
// 如果需要更多工作线程,增加最小线程数
ThreadPool.SetMinThreads(
workerThreads *2,
completionPortThreads
);
}
但要记住,你应该在什么时候调整线程池设置呢?
4️⃣ 要避免的常见错误
async void
// 不好:无法正确处理错误
publicasyncvoidProcessData()// 🚫
{
await Task.Delay(100);
}
// 好:返回 Task,以便可以处理错误
publicasyncTaskProcessData()// ✅
{
await Task.Delay(100);
}
不必要的异步
// 不好:浪费的异步开销
publicasyncTask<int>GetNumber()// 🚫
{
returnawait Task.FromResult(42);
}
// 好:当没有真正的异步工作时直接返回
publicintGetNumber()// ✅
{
return42;
}
记住这些要点:
针对不同情况的实用建议 对于 Web API:
public asyncTask<IActionResult>GetUserData(int userId)
{
// 首先尝试缓存(快速路径)
if(_cache.TryGetValue(userId,outvar userData))
{
returnOk(userData);
}
// 如果不在缓存中,从数据库获取
userData =await _database.GetUserAsync(userId);
// 保存到缓存中以备下次使用
_cache.Set(userId, userData, TimeSpan.FromMinutes(10));
returnOk(userData);
}
对于后台处理:
public asyncTaskProcessQueue()
{
while(!_cancellationToken.IsCancellationRequested)
{
// 批量处理项目以获得更好的性能
var items =await _queue.GetItemsBatchAsync(maxItems:100);
// 一起处理这批项目
await Task.WhenAll(items.Select(ProcessItemAsync));
// 短暂延迟以防止出现紧密循环
await Task.Delay(100);
}
}
对于 UI:同步上下文 同步上下文对于 UI 应用程序和 ASP.NET 至关重要。以下是有效处理它的方法:
理解同步上下文对于应用程序性能至关重要,尤其是在 UI 应用程序中:
public classSynchronizationContextExample
{
publicasyncTaskUIOperation()
{
// 捕获 UI 上下文
var currentContext = SynchronizationContext.Current;
await Task.Run(()=>
{
// 在后台线程上进行繁重的工作
}).ConfigureAwait(false);// 避免切换回 UI 上下文
// 如果需要,手动恢复上下文
if(currentContext !=null)
{
await currentContext.PostAsync(()=>
{
// UI 更新
});
}
}
}
- 在库代码中使用
ConfigureAwait(false)
学习的最佳方法是: