LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

揭开.NET 中 async/await 的神秘面纱:隐藏代价与优化之道

admin
2025年3月22日 22:56 本文热度 114


作为资深的.NET 开发人员,我们都曾将 async/await 用作处理异步操作的常用模式。它简洁、直观,并且使我们的代码更易于维护。然而,在这种优秀的语法背后,隐藏着一套复杂的机制,一旦被误用,可能会对应用程序的性能产生重大影响。

本文将揭示其中隐藏的代价,并探讨每一位经验丰富的开发人员都应该了解的优化策略。

目录

  • 理解基础原理
  • 让你的代码运行得更快:ValueTask
  • 性能提示✓
  • 针对不同情况的实用建议
  • 结论

理解基础原理 .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;
}

为什么这很重要(对性能的影响) 这种转换会带来一些代价:

  1. 内存使用:每个异步方法都需要额外的内存来:
    • 存储状态机
    • 跟踪局部变量
    • 创建 Task 对象
  2. 速度:会有一些额外的工作要做:
    • 设置状态机
    • 在代码的不同部分之间切换
    • 管理所有这些部分

看看这个…

// 简单但可能会浪费资源
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(+ 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)
  • 在对性能要求严格的路径中谨慎处理上下文切换
  • 理解上下文捕获和恢复的代价

学习的最佳方法是:

  • 编写能正常工作的最简单的代码
  • 测量其性能
  • 当发现特定问题时寻求帮助
  • 从你代码库中的实际示例中学习


阅读原文:原文链接


该文章在 2025/3/24 17:09:46 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved