VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#网络编程之异步编程常见陷阱(死锁、资源泄漏)

第五部分:网络编程高级特性
第11章 异步编程常见陷阱
11.1 异步编程常见陷阱(死锁、资源泄漏)
一、我踩过的异步“坑”:死锁让服务器直接崩溃
刚做异步HTTP服务器时,我在控制器方法里用了Task.Result,结果压测时服务器直接死锁——所有线程都被阻塞,请求全部超时。后来排查了3天,才发现是同步上下文导致的死锁。这节我把自己踩过的坑、踩坑后的总结都揉进去,用大白话讲vb.net教程C#教程python教程SQL教程access 2010教程透异步编程的常见陷阱,结合代码示例,让你避开这些坑,写出稳定的异步程序。
二、陷阱一:死锁——异步编程的“头号杀手”
死锁是异步编程最常见的问题,也是最难排查的问题之一。死锁的本质是两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行。

  1. 最常见的死锁:同步上下文+Task.Wait()/Task.Result
    这是异步编程中最容易犯的错误,尤其是在UI线程或ASP.NET(非Core)中。
    场景:在有同步上下文的线程(比如UI线程、ASP.NET线程)里,用Task.Wait()或Task.Result等待异步方法完成。
    示例:UI线程的死锁
    csharp
	using System;
	using System.Threading.Tasks;
	using System.Windows.Forms;
	
	namespace AsyncDeadlockDemo;
	
	public partial class Form1 : Form
	{
	public Form1()
	{
	InitializeComponent();
	}
	
	// 异步方法:模拟网络请求
	private async Task<string> GetDataAsync()
	{
	await Task.Delay(1000); // 模拟网络延迟
	return "Hello World";
	}
	
	// 按钮点击事件:同步调用异步方法,导致死锁
	private void btnGetData_Click(object sender, EventArgs e)
	{
	// 错误:用Task.Result等待异步方法完成,导致死锁
	string data = GetDataAsync().Result;
	MessageBox.Show(data);
	}
	}

死锁的原因:
1.GetDataAsync()被调用,执行到await Task.Delay(1000),捕获当前的UI线程同步上下文;
2.Task.Delay(1000)完成后,GetDataAsync()需要回到UI线程上下文继续执行;
3.但UI线程被GetDataAsync().Result阻塞,无法处理GetDataAsync()的后续代码;
4.结果就是GetDataAsync()等UI线程释放,UI线程等GetDataAsync()返回,互相等,死锁。
正确做法:用await调用异步方法,不要用Task.Wait()或Task.Result
csharp

	// 正确:用await调用异步方法,不会导致死锁
	private async void btnGetData_Click(object sender, EventArgs e)
	{
	string data = await GetDataAsync();
	MessageBox.Show(data);
	}
  1. 另一种死锁:嵌套异步方法的同步等待
    示例:嵌套异步方法的死锁
    csharp
	// 异步方法A
	private async Task<string> MethodAAsync()
	{
	await Task.Delay(1000);
	return "A";
	}
	
	// 异步方法B:调用方法A
	private async Task<string> MethodBAsync()
	{
	return await MethodAAsync();
	}
	
	// 同步方法:调用方法B,导致死锁
	private string MethodC()
	{
	// 错误:用Task.Result等待方法B完成,导致死锁
	return MethodBAsync().Result;
	}

解决方法:把同步方法改成异步方法,用await调用
csharp

	// 正确:把同步方法改成异步方法
	private async Task<string> MethodCAsync()
	{
	return await MethodBAsync();
	}
  1. 如何避免死锁?
    永远不要在有同步上下文的线程里用Task.Wait()或Task.Result;
    所有等待都要用await,而不是同步等待;
    在异步方法里用ConfigureAwait(false),告诉await不需要回到原来的同步上下文(UI线程除外)。
    示例:用ConfigureAwait(false)避免死锁
    csharp
	// 异步方法:用ConfigureAwait(false)避免回到原来的同步上下文
	private async Task<string> GetDataAsync()
	{
	// 用ConfigureAwait(false),不需要回到原来的同步上下文
	await Task.Delay(1000).ConfigureAwait(false);
	return "Hello World";
	}
	
	// 同步方法:调用异步方法,不会导致死锁(因为GetDataAsync()不需要回到原来的同步上下文)
	private string MethodC()
	{
	// 这里不会死锁,但不推荐这么做——还是应该用await调用
	return GetDataAsync().Result;
	}

注意:在UI线程里不要用ConfigureAwait(false),否则更新UI的代码会在非UI线程里执行,导致异常。
三、陷阱二:资源泄漏——异步编程的“隐形杀手”
资源泄漏是指程序在使用完资源后没有正确释放,导致资源被永久占用,最终耗尽系统资源(比如内存、文件句柄、网络连接)。

  1. 最常见的资源泄漏:忘记释放异步资源
    示例:忘记释放HttpClient
    csharp
	// 错误:每次请求都创建新的HttpClient,导致端口耗尽
	public async Task<string> GetDataAsync()
	{
	var httpClient = new HttpClient();
	var response = await httpClient.GetAsync("https://example.com");
	return await response.Content.ReadAsStringAsync();
	}

问题:HttpClient会创建一个底层的HttpMessageHandler,而HttpMessageHandler会占用TCP端口——每次创建新的HttpClient,会导致端口被占用,最终端口耗尽,无法发起新的网络请求。
正确做法:用单例HttpClient或IHttpClientFactory
csharp

	// 正确:用单例HttpClient,避免端口耗尽
	private static readonly HttpClient _httpClient = new HttpClient();
	
	public async Task<string> GetDataAsync()
	{
	var response = await _httpClient.GetAsync("https://example.com");
	return await response.Content.ReadAsStringAsync();
	}

或者用IHttpClientFactory(推荐):
csharp

	// 在Program.cs中注册IHttpClientFactory
	builder.Services.AddHttpClient();
	
	// 在控制器中注入IHttpClientFactory
	private readonly IHttpClientFactory _httpClientFactory;
	
	public UsersController(IHttpClientFactory httpClientFactory)
	{
	_httpClientFactory = httpClientFactory;
	}
	
	public async Task<string> GetDataAsync()
	{
	var httpClient = _httpClientFactory.CreateClient();
	var response = await httpClient.GetAsync("https://example.com");
	return await response.Content.ReadAsStringAsync();
	}
  1. 另一种资源泄漏:忘记取消异步操作
    如果异步操作需要很长时间才能完成,而用户已经取消了操作(比如关闭了窗口),但程序没有取消异步操作,会导致资源被浪费。
    示例:忘记取消异步操作
    csharp
	// 异步方法:模拟长时间的网络请求
	private async Task<string> LongRunningTaskAsync()
	{
	await Task.Delay(10000); // 模拟10秒的网络请求
	return "Done";
	}
	
	// 窗口关闭事件:忘记取消异步操作
	private void Form1_FormClosing(object sender, FormClosingEventArgs e)
	{
	// 错误:没有取消正在执行的异步操作,导致异步操作继续执行,浪费资源
	}

正确做法:用CancellationToken取消异步操作
csharp

	private CancellationTokenSource _cts;
	
	// 按钮点击事件:启动异步操作
	private async void btnStart_Click(object sender, EventArgs e)
	{
	_cts = new CancellationTokenSource();
	try
	{
	string result = await LongRunningTaskAsync(_cts.Token);
	MessageBox.Show(result);
	}
	catch (OperationCanceledException)
	{
	MessageBox.Show("操作已取消");
	}
	}
	
	// 异步方法:支持CancellationToken
	private async Task<string> LongRunningTaskAsync(CancellationToken cancellationToken)
	{
	// 把CancellationToken传给异步操作
	await Task.Delay(10000, cancellationToken);
	return "Done";
	}
	
	// 窗口关闭事件:取消异步操作
	private void Form1_FormClosing(object sender, FormClosingEventArgs e)
	{
	_cts?.Cancel();
	_cts?.Dispose();
	}
  1. 如何避免资源泄漏?
    用using语句释放实现了IDisposable的资源:比如HttpClient、SqlConnection、Stream等;
    用单例或池化资源:比如HttpClient、DbConnection等,避免频繁创建和销毁;
    用CancellationToken取消不需要的异步操作:比如用户取消操作、窗口关闭时;
    避免异步方法的无限循环:比如while(true)没有退出条件,导致异步方法永远不完成,资源被永久占用。
    四、陷阱三:异步方法的返回值错误
    异步方法的返回值必须是Task、Task或ValueTask,如果返回值错误,会导致程序行为异常。
  2. 错误:异步方法返回void(除了事件处理程序)
    示例:异步方法返回void
    csharp
	// 错误:异步方法返回void,无法捕获异常,也无法等待它完成
	private async void GetDataAsync()
	{
	await Task.Delay(1000);
	throw new Exception("错误");
	}

问题:
无法用await等待这个方法完成;
方法抛出的异常无法被捕获,会导致程序崩溃;
无法知道方法是否完成。
正确做法:异步方法返回Task或Task
csharp

	// 正确:异步方法返回Task,能捕获异常,也能等待它完成
	private async Task GetDataAsync()
	{
	await Task.Delay(1000);
	throw new Exception("错误");
	}
	
	// 调用异步方法,捕获异常
	private async void btnGetData_Click(object sender, EventArgs e)
	{
	try
	{
	await GetDataAsync();
	}
	catch (Exception ex)
	{
	MessageBox.Show($"错误:{ex.Message}");
	}
	}
  1. 错误:异步方法返回null
    示例:异步方法返回null
    csharp
	// 错误:异步方法返回null,导致NullReferenceException
	private Task<string> GetDataAsync()
	{
	return null;
	}
	
	// 调用异步方法,导致NullReferenceException
	private async void btnGetData_Click(object sender, EventArgs e)
	{
	string data = await GetDataAsync();
	}

正确做法:异步方法返回Task.FromResult或Task.CompletedTask
csharp

	// 正确:异步方法返回Task.FromResult,不会返回null
	private Task<string> GetDataAsync()
	{
	return Task.FromResult("Hello World");
	}
	
	// 无返回值的异步方法,返回Task.CompletedTask
	private Task DoSomethingAsync()
	{
	return Task.CompletedTask;
	}

五、陷阱四:异步方法的异常处理
异步方法的异常处理和同步方法不同,如果处理不当,会导致异常被忽略,程序行为异常。

  1. 错误:忽略异步方法的异常
    示例:忽略异步方法的异常
    csharp
	// 错误:调用异步方法但不等待,异常被忽略
	private void btnGetData_Click(object sender, EventArgs e)
	{
	GetDataAsync(); // 异常被忽略,程序不会崩溃,但也不会处理异常
	}
	
	private async Task GetDataAsync()
	{
	await Task.Delay(1000);
	throw new Exception("错误");
	}

正确做法:用await调用异步方法,捕获异常
csharp

	// 正确:用await调用异步方法,捕获异常
	private async void btnGetData_Click(object sender, EventArgs e)
	{
	try
	{
	await GetDataAsync();
	}
	catch (Exception ex)
	{
	MessageBox.Show($"错误:{ex.Message}");
	}
	}
  1. 错误:在异步方法里用try-catch包裹await
    示例:在异步方法里用try-catch包裹await
    csharp
	// 错误:try-catch包裹await,导致异常被捕获,但无法传递给调用者
	private async Task<string> GetDataAsync()
	{
	try
	{
	await Task.Delay(1000);
	throw new Exception("错误");
	}
	catch (Exception ex)
	{
	// 异常被捕获,但没有重新抛出,调用者无法知道发生了错误
	return "Error";
	}
	}

正确做法:要么在异步方法里处理异常,要么重新抛出异常
csharp

	// 正确:重新抛出异常,让调用者处理
	private async Task<string> GetDataAsync()
	{
	try
	{
	await Task.Delay(1000);
	throw new Exception("错误");
	}
	catch (Exception ex)
	{
	// 记录日志
	Console.WriteLine($"异常:{ex.Message}");
	// 重新抛出异常
	throw;
	}
	}

六、基础知识拓展

  1. 什么是同步上下文(SynchronizationContext)?
    同步上下文是一个抽象类,用来表示线程的上下文,比如UI线程的同步上下文会保证代码在UI线程执行,ASP.NET的同步上下文会保证代码在同一个请求的线程执行。
    查看当前线程的同步上下文:
    csharp
	SynchronizationContext currentContext = SynchronizationContext.Current;
	if (currentContext != null)
	{
	Console.WriteLine("当前线程有同步上下文");
	}
	else
	{
	Console.WriteLine("当前线程没有同步上下文");
	}

注意:线程池线程默认没有同步上下文,异步方法在没有同步上下文的线程里执行时,await不会捕获同步上下文,而是在线程池线程里继续执行。
2. 如何检测死锁?
Visual Studio调试:在调试时,查看线程窗口,看是否有线程处于“等待”状态,并且互相等待;
用工具检测:比如Visual Studio的“并发可视化工具”、JetBrains dotTrace等;
日志:在异步方法的关键步骤打日志,看是否有步骤没有执行。
3. 异步方法的最佳实践
异步方法名以Async结尾:比如GetDataAsync、SaveDataAsync;
异步方法不要返回void:除了事件处理程序,异步方法应该返回Task或Task;
用ConfigureAwait(false)提高性能:在不需要回到同步上下文的异步方法里,用ConfigureAwait(false);
用CancellationToken支持取消操作:所有长时间运行的异步方法都应该支持CancellationToken;
用using语句释放资源:所有实现了IDisposable的资源都应该用using语句释放。
七、总结:异步编程的“避坑指南”
1.避免死锁:永远不要在有同步上下文的线程里用Task.Wait()或Task.Result,所有等待都要用await;
2.避免资源泄漏:用using语句释放资源,用单例或池化资源,用CancellationToken取消不需要的异步操作;
3.正确处理返回值:异步方法返回Task或Task,不要返回void或null;
4.正确处理异常:用await调用异步方法,捕获异常,不要忽略异常;
5.最佳实践:异步方法名以Async结尾,用ConfigureAwait(false)提高性能,用CancellationToken支持取消操作。

 本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49526.html


相关教程