问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C#异步编程:提升应用性能的最佳实践

创作时间:
作者:
@小白创作中心

C#异步编程:提升应用性能的最佳实践

引用
搜狐
12
来源
1.
http://m.sohu.com/a/741701010_121124364/?pvid=000115_3w_a
2.
https://blog.csdn.net/qq_26900081/article/details/122230605
3.
https://blog.csdn.net/dzweather/article/details/132656741
4.
https://m.blog.csdn.net/qq_43024228/article/details/106112509
5.
https://m.blog.csdn.net/oSenLin123456/article/details/133376017
6.
https://learn.microsoft.com/zh-cn/archive/msdn-magazine/2011/october/asynchronous-programming-async-performance-understanding-the-costs-of-async-and-await
7.
https://www.cnblogs.com/ms27946/p/ConfigureAwait_in_NET8_CN.html
8.
https://www.cnblogs.com/wei325/p/15957951.html#_label1
9.
https://www.cnblogs.com/wei325/p/15957951.html
10.
https://www.cnblogs.com/souphm/p/10931576.html
11.
https://www.cnblogs.com/myzony/p/10647460.html
12.
https://juejin.cn/post/7089655118660337700

在现代软件开发中,C#的异步编程是处理耗时操作并提升应用程序响应性的核心技术之一。通过正确使用async/await关键字以及ConfigureAwait(false)方法,开发者能够显著提高I/O密集型任务的执行效率,并避免不必要的上下文切换问题。本文深入探讨了这些最佳实践,帮助你掌握如何编写高效、可维护的异步代码。

01

异步编程基础回顾

在C#中,asyncawait关键字用于简化异步编程,使代码更易于编写和理解。异步编程允许程序在执行耗时操作时继续响应其他任务,避免阻塞主线程。

  • 异步方法:使用async关键字标记,可以包含await表达式。
  • 等待操作await关键字暂停方法的执行,直到等待的任务完成,然后恢复执行。
public static async Task Method1()
{
    await Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Method 1");
        }
    });
}

在这个例子中,Method1是一个异步方法,它使用Task.Run在后台线程上执行一个循环。通过await关键字,我们等待这个任务完成,但不会阻塞主线程。

02

性能优化关键点

理解异步方法的实现机制

虽然异步方法看起来像普通的同步方法,但编译器会为其生成额外的状态机代码。例如,下面这个简单的异步方法:

public static async Task SimpleBodyAsync()
{
    Console.WriteLine("Hello, Async World!");
}

在编译后会变成:

[DebuggerStepThrough]      
public static Task SimpleBodyAsync() {
  <SimpleBodyAsync>d__0 d__ = new <SimpleBodyAsync>d__0();
  d__.<>t__builder = AsyncTaskMethodBuilder.Create();
  d__.MoveNext();
  return d__.<>t__builder.Task;
}

[CompilerGenerated]
[StructLayout(LayoutKind.Sequential)]
private struct <SimpleBodyAsync>d__0 : <>t__IStateMachine {
  private int <>1__state;
  public AsyncTaskMethodBuilder <>t__builder;
  public Action <>t__MoveNextDelegate;

  public void MoveNext() {
    try {
      if (this.<>1__state == -1) return;
      Console.WriteLine("Hello, Async World!");
    }
    catch (Exception e) {
      this.<>1__state = -1;
      this.<>t__builder.SetException(e);
      return;
    }

    this.<>1__state = -1;
    this.<>t__builder.SetResult();
  }

  ...
}

可以看到,一个简单的异步方法实际上生成了两个方法和一个状态机结构体。这说明异步方法的调用成本远高于普通方法。

使用ConfigureAwait(false)优化上下文切换

默认情况下,await关键字会捕获当前的同步上下文,并在任务完成后恢复该上下文。这在UI线程或ASP.NET请求上下文中特别重要,但也会带来额外的性能开销。

通过使用ConfigureAwait(false),我们可以告诉编译器不需要恢复上下文,从而避免不必要的上下文切换:

public static async Task<JObject> GetJsonAsync(Uri uri)
{
    using (var client = new HttpClient)
    {
        var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
        return JObject.Parse(jsonString);
    }
}

但是需要注意,ConfigureAwait(false)并不意味着一定会在不同的线程上执行后续代码。它只在await暂停执行并稍后恢复异步方法时生效。如果await的任务已经完成,它将不会暂停执行;在这种情况下,ConfigureAwait将不会起作用。

03

最佳实践

避免主线程阻塞

在UI界面或ASP.NET应用中,如果异步代码使用不当,很容易导致线程阻塞。例如:

public void Button1_Click(...)
{
    var jsonTask = GetJsonAsync(...);
    textBox1.Text = jsonTask.Result;
}

解决方法是使用async/await

public async void Button1_Click(...)
{
    var json = await GetJsonAsync(...);
    textBox1.Text = json;
}

或者使用ConfigureAwait(false)

public static async Task<JObject> GetJsonAsync(Uri uri)
{
    using (var client = new HttpClient)
    {
        var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
        return JObject.Parse(jsonString);
    }
}

正确处理I/O密集型任务

对于I/O密集型操作,如文件读写、网络请求等,应该优先使用异步API:

static async Task<int> ReadFileAsync(string filePath)
{
    using (StreamReader reader = new StreamReader(filePath))
    {
        string content = await reader.ReadToEndAsync();
        return content.Length;
    }
}

避免使用async void

async void方法在异常处理方面存在隐患,应尽量避免使用。改为使用TaskTask<T>作为返回类型:

private async Task Test()
{
    using DbController controller = new DbController();
    var re = await controller.GetRecordsAsync(8888);
    throw new Exception("哎呀,这里怎么会出现异常");
}

[HttpGet("Demo")]
public ActionResult Demo()
{
    _ = Test();
    return Ok("");
}

命名规范

异步方法的命名应以Async结尾,便于识别:

public static async Task<int> Method1Async()
{
    int count = 0;
    await Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Method 1");
            count++;
        }
    });
    return count;
}
04

常见错误和注意事项

异常处理

异步代码中的异常需要特别处理,通常需要在await调用周围使用try/catch

public static async Task<int> Method1Async()
{
    try
    {
        int count = 0;
        await Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("Method 1");
                count++;
            }
        });
        return count;
    }
    catch (Exception ex)
    {
        // 异常处理
    }
}

避免过度使用异步

并不是所有场景都需要异步,过度使用异步反而会带来额外的性能开销。对于CPU密集型任务,如果没有I/O操作,可以考虑使用同步方法。

正确理解ConfigureAwait(false)

  • ConfigureAwait(false)并不是避免死锁的好方法。
  • 它配置的是await,而不是任务。
  • 它并不意味着“在线程池线程上运行此方法的后续部分”或“在不同的线程上运行此方法的后续部分”。

通过合理使用async/awaitConfigureAwait(false),开发者可以编写出既高效又易于维护的异步代码。在实际开发中,需要根据具体场景选择合适的异步策略,同时注意避免常见的陷阱。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号