C#异步编程:提升应用性能的最佳实践
C#异步编程:提升应用性能的最佳实践
在现代软件开发中,C#的异步编程是处理耗时操作并提升应用程序响应性的核心技术之一。通过正确使用async/await
关键字以及ConfigureAwait(false)
方法,开发者能够显著提高I/O密集型任务的执行效率,并避免不必要的上下文切换问题。本文深入探讨了这些最佳实践,帮助你掌握如何编写高效、可维护的异步代码。
异步编程基础回顾
在C#中,async
和await
关键字用于简化异步编程,使代码更易于编写和理解。异步编程允许程序在执行耗时操作时继续响应其他任务,避免阻塞主线程。
- 异步方法:使用
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
关键字,我们等待这个任务完成,但不会阻塞主线程。
性能优化关键点
理解异步方法的实现机制
虽然异步方法看起来像普通的同步方法,但编译器会为其生成额外的状态机代码。例如,下面这个简单的异步方法:
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
将不会起作用。
最佳实践
避免主线程阻塞
在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
方法在异常处理方面存在隐患,应尽量避免使用。改为使用Task
或Task<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;
}
常见错误和注意事项
异常处理
异步代码中的异常需要特别处理,通常需要在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/await
和ConfigureAwait(false)
,开发者可以编写出既高效又易于维护的异步代码。在实际开发中,需要根据具体场景选择合适的异步策略,同时注意避免常见的陷阱。