VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#网络编程之.NET异步模型:Async/Await、Task、ValueTask深度解析

.NET异步模型:Async/Await、Task、ValueTask深度解析
引言:为什么需要异步编程?
在传统同步编程中,代码按顺序执行,遇到IO操作(如数据库查询、网络请求)时,线程会被阻塞,直到IO完成。例如:
csharp

	// 同步代码:线程阻塞1秒等待IO完成 
	var result = await GetDataFromDatabase(); // 假设耗时1秒

此时,线程什么也做不了,只能等待,造成资源浪费(尤其是高并vb.net教程C#教程python教程SQL教程access 2010教程发场景下,大量线程阻塞会导致CPU上下文切换频繁,性能下降)。
异步编程的核心目标:让线程在等待IO时去处理其他任务,提高资源利用率和系统吞吐量。.NET提供了一套完整的异步模型,核心组件包括Task/ValueTask、async/await关键字,以及底层的状态机机制。
18.7.2 基础组件:Task与ValueTask

  1. Task:异步操作的“承诺”
    Task是.NET异步编程的基础,代表一个可能尚未完成的异步操作,可以理解为“未来会返回结果的承诺”。它有两种常用形式:
    Task:无返回值的异步操作;
    Task:有返回值的异步操作(TResult为返回类型)。
    核心特性
    状态管理:Task包含四种状态(Created、WaitingForActivation、Running、RanToCompletion、Faulted、Canceled),通过Status属性可查询;
    结果获取:通过Result属性(同步阻塞获取)或await关键字(异步等待,非阻塞)获取结果;
    异常处理:异步操作抛出的异常会被捕获到Task中,可通过Exception属性或try/catch块处理。
    代码示例:创建并执行Task
    csharp
	using System; 
	using System.Threading.Tasks; 
	
	class TaskDemo 
	{ 
	static void Main() 
	{ 
	// 1. 创建Task并启动(无返回值) 
	Task task1 = Task.Run(() => 
	{ 
	Console.WriteLine("Task1开始执行"); 
	System.Threading.Thread.Sleep(1000); // 模拟IO操作(实际应使用异步API) 
	Console.WriteLine("Task1执行完成"); 
	}); 
	
	// 2. 创建有返回值的Task 
	Task<int> task2 = Task.Run(() => 
	{ 
	Console.WriteLine("Task2开始执行"); 
	System.Threading.Thread.Sleep(1500); 
	return 42; // 返回结果 
	}); 
	
	Console.WriteLine("主线程继续执行其他任务..."); 
	
	// 3. 等待Task完成(同步阻塞,仅示例,实际应使用await) 
	task1.Wait(); 
	int result = task2.Result; 
	Console.WriteLine($"Task2返回结果:{result}"); 
	} 
	}

输出顺序:

	主线程继续执行其他任务... 
	Task1开始执行 
	Task2开始执行 
	Task1执行完成 
	Task2执行完成 
	Task2返回结果:42

关键注意点:
Task.Run():将任务放入线程池执行,避免阻塞主线程;
Wait()/Result:同步阻塞线程,仅在必要时使用(如控制台程序的Main方法),异步代码中应使用await;
线程池:Task.Run()默认使用.NET线程池,线程池会自动管理线程数量(避免创建过多线程导致资源耗尽)。
2. ValueTask:高性能异步操作的优化
ValueTask(.NET Core 2.1+引入)是Task的值类型替代方案,用于优化同步完成概率高的异步场景(如缓存查询:80%的请求命中缓存,同步返回;20%未命中,异步查询数据库)。
为什么需要ValueTask?
Task是引用类型,每次创建都会在堆上分配内存(约40字节),在高并发场景下会产生大量GC压力。而ValueTask是值类型,若异步操作同步完成,可直接返回结果,无需堆分配;若异步完成,则内部会创建一个Task(延迟分配)。
适用场景
异步操作大概率同步完成(如缓存、本地文件读取);
高并发场景(如Web API接口,每秒数万次请求)。
代码示例:ValueTask vs Task
csharp

	using System; 
	using System.Threading.Tasks; 
	
	class ValueTaskDemo 
	{ 
	// 模拟缓存:key为"cache"时同步返回,否则异步查询数据库 
	public async ValueTask<string> GetDataAsync(string key) 
	{ 
	// 1. 同步命中缓存(无堆分配) 
	if (key == "cache") 
	{ 
	return "缓存数据"; // 直接返回值,无需创建Task 
	} 
	
	// 2. 异步查询数据库(延迟创建Task) 
	await Task.Delay(1000); // 模拟IO操作 
	return "数据库数据"; 
	} 
	
	static async Task Main() 
	{ 
	var demo = new ValueTaskDemo(); 
	
	// 测试缓存命中(同步完成) 
	string cacheResult = await demo.GetDataAsync("cache"); 
	Console.WriteLine(cacheResult); // 输出:缓存数据 
	
	// 测试未命中(异步完成) 
	string dbResult = await demo.GetDataAsync("db"); 
	Console.WriteLine(dbResult); // 输出:数据库数据 
	} 
	}

关键注意点:
ValueTask不能被多次等待:若ValueTask已完成,再次等待会抛出异常(需调用AsTask()转换为Task后再等待);
避免滥用:若异步操作几乎总是异步完成(如网络请求),使用ValueTask会增加额外开销(需检查是否同步完成),此时应使用Task。
18.7.3 核心语法:async/await关键字
async/await是.NET异步编程的“语法糖”,它让异步代码的写法和同步代码几乎一致,大大降低了异步编程的学习成本。其底层是.NET编译器自动生成的状态机(State Machine)。

  1. 基本用法
    async:标记方法为异步方法,返回类型必须是void、Task或Task(void仅用于事件处理程序);
    await:等待异步操作完成,线程不会被阻塞,而是返回调用方继续执行其他任务。
    代码示例:async/await的基本使用
    csharp
	using System; 
	using System.Net.Http; 
	using System.Threading.Tasks; 
	
	class AsyncAwaitDemo 
	{ 
	// 异步方法:返回Task<string>,表示异步操作返回string结果 
	public async Task<string> FetchWebPageAsync(string url) 
	{ 
	using (HttpClient client = new HttpClient()) 
	{ 
	// await等待异步操作完成,线程不阻塞 
	string html = await client.GetStringAsync(url); 
	return html; 
	} 
	} 
	
	static async Task Main() 
	{ 
	var demo = new AsyncAwaitDemo(); 
	try 
	{ 
	// 异步等待网页内容 
	string html = await demo.FetchWebPageAsync("https://www.baidu.com"); 
	Console.WriteLine($"网页长度:{html.Length}"); 
	} 
	catch (HttpRequestException ex) 
	{ 
	// 异常处理:异步方法的异常会被捕获到try/catch块中 
	Console.WriteLine($"请求失败:{ex.Message}"); 
	} 
	} 
	}

关键注意点:
异步方法命名规范:以Async结尾(如FetchWebPageAsync);
await只能在async方法中使用;
异常处理:异步方法抛出的异常会被await重新抛出,可通过try/catch块捕获。
2. 底层原理:状态机机制
async/await的底层是.NET编译器自动生成的状态机。例如,上述FetchWebPageAsync方法会被编译器转换为类似以下的代码(简化版):
csharp

	// 编译器自动生成的状态机类 
	private sealed class <FetchWebPageAsync>d__0 : IAsyncStateMachine 
	{ 
	public int <>1__state; 
	public AsyncTaskMethodBuilder<string> <>t__builder; 
	public AsyncAwaitDemo <>4__this; 
	public string url; 
	public HttpClient <client>5__1; 
	public string <html>5__2; 
	private TaskAwaiter<string> <>u__1; 
	
	void IAsyncStateMachine.MoveNext() 
	{ 
	int num = <>1__state; 
	string result = null; 
	try 
	{ 
	TaskAwaiter<string> awaiter; 
	if (num != 0) 
	{ 
	// 第一次执行:创建HttpClient,调用GetStringAsync 
	<client>5__1 = new HttpClient(); 
	awaiter = <client>5__1.GetStringAsync(url).GetAwaiter(); 
	if (!awaiter.IsCompleted) 
	{ 
	num = (<>1__state = 0); 
	<>u__1 = awaiter; 
	<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); 
	return; 
	} 
	} 
	else 
	{ 
	// 第二次执行:异步操作完成,获取结果 
	awaiter = <>u__1; 
	<>u__1 = default; 
	num = (<>1__state = -1); 
	} 
	<html>5__2 = awaiter.GetResult(); // 获取异步结果 
	result = <html>5__2; 
	<client>5__1.Dispose(); // 释放资源 
	} 
	catch (Exception exception) 
	{ 
	<>1__state = -2; 
	<>t__builder.SetException(exception); 
	return; 
	} 
	<>1__state = -2; 
	<>t__builder.SetResult(result); // 返回结果 
	} 
	
	void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) 
	{ 
	<>t__builder.SetStateMachine(stateMachine); 
	} 
	}

状态机执行流程:
1.第一次调用:状态机处于初始状态(<>1__state = -1),执行HttpClient.GetStringAsync(url),若操作未完成,将状态机挂起,返回调用方;
2.异步操作完成:状态机被唤醒,从挂起点继续执行,获取结果并返回;
3.资源清理:自动调用Dispose()释放HttpClient资源(因使用了using语句)。
核心优势:状态机自动管理异步操作的状态,开发者无需手动处理回调和线程切换,代码更简洁易读。
3. 常见误区:async void与async Task的区别
async void仅用于事件处理程序(如WPF的按钮点击事件),在其他场景下应避免使用,原因如下:
异常无法捕获:async void方法抛出的异常会直接抛到调用线程的同步上下文,可能导致程序崩溃;
无法等待:async void方法无法被await,调用方无法知道操作何时完成;
资源泄漏风险:若方法中包含未完成的异步操作,程序可能在操作完成前退出。
错误示例:async void的风险
csharp

	// 错误示例:非事件处理程序使用async void 
	async void BadAsyncMethod() 
	{ 
	await Task.Delay(1000); 
	throw new InvalidOperationException("异步方法抛出异常"); // 异常无法被捕获 
	} 
	
	static void Main() 
	{ 
	try 
	{ 
	BadAsyncMethod(); // 调用async void方法 
	} 
	catch (Exception ex) 
	{ 
	// 无法捕获异常,程序会崩溃 
	Console.WriteLine($"捕获异常:{ex.Message}"); 
	} 
	Console.ReadLine(); 
	}

正确做法:使用async Task替代async void,并通过await等待方法完成:
csharp

	async Task GoodAsyncMethod() 
	{ 
	await Task.Delay(1000); 
	throw new InvalidOperationException("异步方法抛出异常"); 
	} 
	
	static async Task Main() 
	{ 
	try 
	{ 
	await GoodAsyncMethod(); // 等待异步方法完成,可捕获异常 
	} 
	catch (Exception ex) 
	{ 
	Console.WriteLine($"捕获异常:{ex.Message}"); // 输出:捕获异常:异步方法抛出异常 
	} 
	}

18.7.4 高级特性:异步流与IAsyncEnumerable
.NET Core 3.0+引入了IAsyncEnumerable,支持异步流式返回结果(即分批返回数据,无需等待所有数据准备完成)。例如,从数据库分页查询数据,每查询一页就返回一页,而不是等待所有页查询完成。
核心组件
IAsyncEnumerable:异步流的接口,代表一个可异步枚举的序列;
await foreach:异步遍历IAsyncEnumerable的关键字;
yield return:在异步方法中返回单个元素(类似同步的yield return)。
代码示例:异步流分页查询数据库
csharp

	using System; 
	using System.Collections.Generic; 
	using System.Threading.Tasks; 
	
	class AsyncStreamDemo 
	{ 
	// 模拟数据库分页查询:每页返回10条数据,共3页 
	public async IAsyncEnumerable<string> QueryDataAsync(int pageSize = 10) 
	{ 
	for (int page = 1; page <= 3; page++) 
	{ 
	// 模拟异步查询数据库(每页耗时500ms) 
	await Task.Delay(500); 
	// 返回当前页的数据 
	for (int i = 1; i <= pageSize; i++) 
	{ 
	yield return $"第{page}页,第{i}条数据"; 
	} 
	} 
	} 
	
	static async Task Main() 
	{ 
	var demo = new AsyncStreamDemo(); 
	// 异步遍历异步流 
	await foreach (string item in demo.QueryDataAsync()) 
	{ 
	Console.WriteLine(item); 
	} 
	} 
	}

输出效果:每500ms输出10条数据,共输出30条,而不是等待1.5秒后一次性输出。
关键注意点:
IAsyncEnumerable方法必须标记为async;
await foreach只能在async方法中使用;
适合大数据量场景:避免一次性加载所有数据到内存,减少内存占用。
18.7.5 性能优化与最佳实践

  1. 避免过度异步化
    异步操作并非“银弹”,若操作本身是CPU密集型(如复杂计算),异步编程可能会增加开销(状态机创建、线程切换)。此时应使用同步代码,或通过Task.Run()将CPU密集型操作放入线程池(避免阻塞UI线程或Web请求线程)。
  2. 合理使用ValueTask
    当异步操作大概率同步完成时使用ValueTask,否则使用Task;
    避免多次等待ValueTask:若需多次等待,调用AsTask()转换为Task后再等待。
  3. 异步方法的异常处理
    始终使用try/catch块捕获异步方法的异常;
    对于批量异步操作(如Task.WhenAll),可通过AggregateException捕获所有异常:
    csharp
	Task task1 = Task.Run(() => throw new Exception("Task1异常")); 
	Task task2 = Task.Run(() => throw new Exception("Task2异常")); 
	
	try 
	{ 
	await Task.WhenAll(task1, task2); 
	} 
	catch (AggregateException ex) 
	{ 
	foreach (var innerEx in ex.InnerExceptions) 
	{ 
	Console.WriteLine($"捕获异常:{innerEx.Message}"); 
	} 
	}
  1. 避免阻塞异步代码
    禁止在异步代码中使用Task.Wait()、Task.Result或Thread.Sleep(),这些会导致线程阻塞;
    若需等待,使用await;若需延迟,使用Task.Delay()(异步延迟,非阻塞)。
    18.7.6 总结:异步编程的核心价值
    .NET异步模型通过Task/ValueTask、async/await关键字和状态机机制,让开发者可以用同步代码的写法实现异步逻辑,同时避免了回调地狱(Callback Hell)。其核心价值在于:
    提高资源利用率:线程在等待IO时可处理其他任务;
    提升系统吞吐量:高并发场景下,异步编程可支持更多请求;
    改善用户体验:UI应用中,异步操作不会导致界面卡顿。
    掌握异步编程是.NET开发者的必备技能,尤其是在Web API、微服务、高性能应用等场景中,异步编程几乎是标配。
    18.7.7 思考题
    1.对比Task和ValueTask的适用场景,举例说明什么情况下应使用ValueTask?
    2.解释async/await的底层状态机机制,说明状态机如何管理异步操作的状态?
    3.为什么async void仅用于事件处理程序?在非事件场景下使用async void会导致什么问题?
    4.如何批量执行多个异步操作,并等待所有操作完成?如何处理其中的异常?
    5.设计一个异步方法,从数据库分页查询数据并返回异步流(IAsyncEnumerable),要求支持取消操作(使用CancellationToken)。

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


相关教程