1. C#异步编程基础概念
在控制台应用中实现异步编程是C#开发者的必备技能。异步编程的核心目标是提高应用程序的响应能力,特别是在执行I/O密集型操作时。传统的同步代码会阻塞当前线程直到操作完成,而异步方法在等待操作完成时会返回控制权给调用方。
1.1 async和await关键字
async和await是C#异步编程模型的核心关键字。async修饰符用于声明一个方法是异步的,而await操作符用于暂停当前方法的执行,直到等待的任务完成。
csharp复制static async Task Main(string[] args)
{
await DoSomethingAsync();
Console.WriteLine("操作完成");
}
static async Task DoSomethingAsync()
{
await Task.Delay(1000); // 模拟耗时操作
}
关键点:
async方法通常返回Task、Task<T>或ValueTask<T>await只能在async方法中使用- 被
await调用的方法通常也应该是异步的
1.2 Task和Task
Task类表示一个异步操作,它有以下重要特性:
Status属性表示任务当前状态Result属性获取任务结果(仅限Task<T>)ContinueWith方法允许指定任务完成后执行的操作
csharp复制static async Task<int> CalculateResultAsync()
{
await Task.Delay(1000); // 模拟计算
return 42;
}
2. 控制台应用中的异步实现
2.1 异步Main方法
从C# 7.1开始,Main方法可以声明为async:
csharp复制static async Task Main(string[] args)
{
Console.WriteLine("开始执行");
await Task.Delay(1000);
Console.WriteLine("1秒后");
}
这种写法比传统的.Wait()或.GetAwaiter().GetResult()更安全,因为它不会导致死锁。
2.2 基本异步模式
控制台应用中最常见的异步模式:
csharp复制static async Task RunAsync()
{
var task1 = DoWorkAsync("任务1", 1500);
var task2 = DoWorkAsync("任务2", 1000);
await Task.WhenAll(task1, task2);
Console.WriteLine("所有任务完成");
}
static async Task DoWorkAsync(string name, int delay)
{
Console.WriteLine($"{name} 开始");
await Task.Delay(delay);
Console.WriteLine($"{name} 完成");
}
3. 实际应用示例
3.1 模拟早餐制作流程
让我们用一个早餐制作的例子来演示异步编程的实际应用:
csharp复制static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("咖啡准备好了");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("鸡蛋准备好了");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("培根准备好了");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("吐司准备好了");
}
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("橙汁准备好了");
Console.WriteLine("早餐完成!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
3.2 并行执行多个任务
使用Task.WhenAll可以并行执行多个独立任务:
csharp复制static async Task ProcessDataAsync()
{
var downloadTask = DownloadDataAsync();
var processTask = ProcessLocalDataAsync();
await Task.WhenAll(downloadTask, processTask);
var downloadedData = await downloadTask;
var processedData = await processTask;
Console.WriteLine($"处理完成,下载数据量:{downloadedData.Length},处理数据量:{processedData.Count}");
}
4. 错误处理与最佳实践
4.1 异步方法中的异常处理
异步方法的异常处理与同步代码类似,但有一些特殊注意事项:
csharp复制static async Task Main(string[] args)
{
try
{
await RiskyOperationAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"操作失败:{ex.Message}");
}
}
static async Task RiskyOperationAsync()
{
await Task.Delay(500);
throw new InvalidOperationException("模拟错误");
}
4.2 避免常见陷阱
- 不要使用async void:除了事件处理程序外,总是返回
Task或Task<T> - 谨慎使用.Result和.Wait():这可能导致死锁
- 配置异步等待:使用
ConfigureAwait(false)避免上下文切换
csharp复制// 不好的做法
var result = SomeAsyncMethod().Result;
// 好的做法
var result = await SomeAsyncMethod().ConfigureAwait(false);
4.3 性能优化技巧
- 使用ValueTask:对于可能同步完成的操作,考虑使用
ValueTask - 避免过度并行化:太多并行任务可能导致资源耗尽
- 合理设置超时:使用
CancellationTokenSource设置超时
csharp复制static async Task<string> FetchWithTimeoutAsync(string url)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
return await httpClient.GetStringAsync(url, cts.Token);
}
catch (TaskCanceledException)
{
return "请求超时";
}
}
5. 高级异步模式
5.1 异步流(IAsyncEnumerable)
C# 8.0引入了异步流,允许异步生成和消费序列:
csharp复制static async IAsyncEnumerable<int> GenerateSequenceAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100);
yield return i;
}
}
static async Task ConsumeSequenceAsync()
{
await foreach (var number in GenerateSequenceAsync())
{
Console.WriteLine(number);
}
}
5.2 异步工厂模式
实现异步对象初始化:
csharp复制public class DataProcessor
{
private DataProcessor() { }
public static async Task<DataProcessor> CreateAsync()
{
var processor = new DataProcessor();
await processor.InitializeAsync();
return processor;
}
private async Task InitializeAsync()
{
// 异步初始化代码
}
}
5.3 异步锁模式
处理需要同步的异步代码:
csharp复制private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1);
public async Task AccessResourceAsync()
{
await _mutex.WaitAsync();
try
{
// 临界区代码
}
finally
{
_mutex.Release();
}
}
6. 调试异步代码的技巧
调试异步代码可能会遇到一些挑战,以下是一些实用技巧:
- 使用Visual Studio的并行堆栈窗口:查看所有活动任务
- 记录任务ID:在日志中包含
Task.Id帮助追踪 - 使用调试器中的"显示并行任务"功能:Visual Studio的强大工具
- 设置任务名称:使用
Task.Factory.StartNew的TaskCreationOptions.LongRunning选项
csharp复制var task = Task.Run(async () =>
{
Debug.WriteLine($"任务ID: {Task.CurrentId}");
await SomeOperationAsync();
});
在实际项目中,我发现最有效的调试方法是良好的日志记录策略。在每个重要异步操作的开始和结束处添加日志,可以大大简化问题诊断过程。
