C#异步和并发在IO密集场景的典型应用 async/await
在 IO 密集场景的核心价值是释放线程,避免线程阻塞等待外部资源,提升程序吞吐量。并发实现的关键是(等待多个异步任务完成),而非逐个await,可大幅降低总耗时。最佳实践:全程异步、控制并发数、避免async void,优先使用.NET 内置的异步 IO 方法(如GetAsync。
在 IO 密集型场景下,C# 中如何利用async/await实现异步和并发。
一、核心概念理解
首先要明确:IO 密集型场景(如网络请求、文件读写、数据库操作)的瓶颈不在于 CPU 计算,而在于等待外部资源响应。async/await的核心价值是释放线程—— 在等待 IO 响应时,当前线程不会被阻塞,可去处理其他任务,大幅提升程序吞吐量。
关键区别(新手必看)
表格
| 同步方式 | 异步方式(async/await) |
|---|---|
| 线程阻塞等待 IO 响应 | 线程释放,IO 完成后回调 |
| 高线程占用,吞吐量低 | 低线程占用,吞吐量高 |
| 代码线性执行,易理解 | 代码看似线性,实际异步执行 |
二、IO 密集场景典型应用(附完整代码)
下面以文件批量读写、多接口并发请求、数据库异步操作三个典型场景为例,给出可直接运行的代码,并解释核心逻辑。
场景 1:批量读取文件(IO 密集)
需求:同时读取多个大文件,避免同步读取导致的阻塞,提升效率。
using System;
using System.IO;
using System.Threading.Tasks;
class FileAsyncDemo
{
// 异步读取单个文件
private static async Task<string> ReadFileAsync(string filePath)
{
try
{
// 使用File类的异步方法,await等待IO完成(不阻塞线程)
return await File.ReadAllTextAsync(filePath);
}
catch (Exception ex)
{
Console.WriteLine($"读取文件{filePath}失败:{ex.Message}");
return string.Empty;
}
}
// 批量异步读取多个文件(并发)
private static async Task BatchReadFilesAsync(string[] filePaths)
{
// 1. 创建所有异步任务(此时任务已开始执行,并发)
var readTasks = new Task<string>[filePaths.Length];
for (int i = 0; i < filePaths.Length; i++)
{
readTasks[i] = ReadFileAsync(filePaths[i]);
}
// 2. 等待所有任务完成(非阻塞,线程可处理其他事)
string[] fileContents = await Task.WhenAll(readTasks);
// 3. 处理结果
for (int i = 0; i < filePaths.Length; i++)
{
Console.WriteLine($"文件{filePaths[i]}内容长度:{fileContents[i].Length}");
}
}
static async Task Main(string[] args)
{
// 测试文件路径(替换为你自己的文件路径)
string[] files = { "file1.txt", "file2.txt", "file3.txt" };
await BatchReadFilesAsync(files);
Console.WriteLine("所有文件读取完成");
}
}
核心解释:
File.ReadAllTextAsync:.NET 内置的异步 IO 方法,底层基于 IOCP(IO 完成端口),无阻塞等待文件读取。Task.WhenAll:等待多个异步任务并发完成,而非逐个等待,是 IO 密集场景并发的核心 API。- 整个过程中,主线程仅在
await处 “挂起”,但线程会被 CLR 回收去处理其他任务,IO 完成后再恢复执行。
场景 2:多接口并发请求(网络 IO)
需求:同时调用多个第三方 API,汇总结果,避免逐个同步请求导致的耗时累加。
using System;
using System.Net.Http;
using System.Threading.Tasks;
class HttpAsyncDemo
{
// 静态HttpClient(避免频繁创建释放,符合最佳实践)
private static readonly HttpClient _httpClient = new HttpClient();
// 异步调用单个API
private static async Task<string> CallApiAsync(string url)
{
try
{
// 异步发送GET请求,await等待响应(不阻塞线程)
HttpResponseMessage response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode(); // 检查是否HTTP 200
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
Console.WriteLine($"调用API{url}失败:{ex.Message}");
return string.Empty;
}
}
// 并发调用多个API
private static async Task BatchCallApisAsync(string[] apiUrls)
{
// 1. 启动所有API请求任务(并发执行)
var apiTasks = new Task<string>[apiUrls.Length];
for (int i = 0; i < apiUrls.Length; i++)
{
apiTasks[i] = CallApiAsync(apiUrls[i]);
}
// 2. 等待所有请求完成,获取结果
string[] apiResults = await Task.WhenAll(apiTasks);
// 3. 处理结果
for (int i = 0; i < apiUrls.Length; i++)
{
Console.WriteLine($"API{apiUrls[i]}返回结果长度:{apiResults[i].Length}");
}
}
static async Task Main(string[] args)
{
// 测试API地址(替换为实际地址)
string[] apis = {
"https://api.example.com/data1",
"https://api.example.com/data2",
"https://api.example.com/data3"
};
Console.WriteLine("开始并发调用API...");
DateTime start = DateTime.Now;
await BatchCallApisAsync(apis);
DateTime end = DateTime.Now;
Console.WriteLine($"所有API调用完成,总耗时:{(end - start).TotalSeconds:F2}秒");
}
}
核心解释:
HttpClient.GetAsync:异步网络请求,等待响应时线程不阻塞,适合高并发的网络 IO 场景。- 注意:
HttpClient需全局复用,不可在每次请求时创建(否则会耗尽端口)。 - 并发请求的总耗时≈最慢的那个 API 响应时间,而非所有 API 耗时之和,这是异步并发的核心优势。
场景 3:数据库异步操作(数据库 IO)
需求:异步执行数据库查询 / 插入,避免阻塞应用线程(尤其 Web 应用中,提升并发处理能力)。
using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
class DbAsyncDemo
{
// 数据库连接字符串(替换为你的配置)
private const string _connectionString = "Server=.;Database=TestDB;Integrated Security=True;";
// 异步查询数据库
private static async Task<int> GetUserCountAsync()
{
using (SqlConnection conn = new SqlConnection(_connectionString))
{
// 1. 异步打开连接(IO操作,无阻塞)
await conn.OpenAsync();
string sql = "SELECT COUNT(*) FROM Users";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
// 2. 异步执行查询(IO操作,无阻塞)
object result = await cmd.ExecuteScalarAsync();
return Convert.ToInt32(result);
}
}
}
static async Task Main(string[] args)
{
try
{
Console.WriteLine("开始查询用户数量...");
int count = await GetUserCountAsync();
Console.WriteLine($"当前用户总数:{count}");
}
catch (Exception ex)
{
Console.WriteLine($"数据库操作失败:{ex.Message}");
}
}
}
核心解释:
OpenAsync/ExecuteScalarAsync:ADO.NET提供的异步数据库操作方法,等待数据库响应时释放线程。- 在 Web 应用(如ASP.NET Core)中,异步数据库操作可大幅提升请求处理能力 —— 同步操作会阻塞线程池线程,而异步操作可让线程处理更多请求。
三、IO 密集场景使用 async/await 的最佳实践
-
全程异步:从底层 IO 操作(如
ReadAllTextAsync)到上层业务逻辑,全部使用async/await,避免 “异步包装同步”(如Task.Run包裹同步 IO 方法),否则无法释放线程,失去异步意义。 -
并发控制:IO 密集场景并发数不宜过高(如 API 请求并发数建议控制在 10-50),可使用
SemaphoreSlim限制并发:// 限制最多10个并发请求 private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(10); private static async Task<string> CallApiWithLimitAsync(string url) { await _semaphore.WaitAsync(); // 等待信号量 try { return await CallApiAsync(url); } finally { _semaphore.Release(); // 释放信号量 } } -
避免 async void:除了事件处理程序,异步方法应返回
Task/Task<T>,便于异常捕获和任务等待。
总结
async/await在 IO 密集场景的核心价值是释放线程,避免线程阻塞等待外部资源,提升程序吞吐量。- 并发实现的关键是
Task.WhenAll(等待多个异步任务完成),而非逐个await,可大幅降低总耗时。 - 最佳实践:全程异步、控制并发数、避免
async void,优先使用.NET 内置的异步 IO 方法(如ReadAllTextAsync、GetAsync、ExecuteScalarAsync)。
更多推荐



所有评论(0)