面试题答案
一键面试异步操作性能优化及避免资源耗尽
- 合理设置线程池参数:
- 可以通过
ThreadPool.SetMinThreads
和ThreadPool.SetMaxThreads
方法设置线程池的最小和最大线程数。例如,如果知道应用程序主要是 I/O 密集型,可以适当降低最小线程数,因为 I/O 操作通常不会占用线程执行,这样可以减少线程上下文切换开销。
// 设置最小工作线程数为10,最小I/O完成线程数为10 ThreadPool.SetMinThreads(10, 10); // 设置最大工作线程数为100,最大I/O完成线程数为100 ThreadPool.SetMaxThreads(100, 100);
- 可以通过
- 避免不必要的异步/同步切换:
- 确保异步方法内部尽量保持异步操作,避免在异步方法中调用同步方法,因为这可能导致线程池线程阻塞,浪费资源。例如,如果有一个异步方法
AsyncMethod
,内部调用了一个同步的数据库查询方法SyncDbQuery
,应将SyncDbQuery
改为异步版本AsyncDbQuery
。
// 错误示例 public async Task<int> AsyncMethod() { return SyncDbQuery(); } // 正确示例 public async Task<int> AsyncMethod() { return await AsyncDbQuery(); }
- 确保异步方法内部尽量保持异步操作,避免在异步方法中调用同步方法,因为这可能导致线程池线程阻塞,浪费资源。例如,如果有一个异步方法
- 使用异步流(
IAsyncEnumerable
):- 对于处理大量数据的异步操作,使用
IAsyncEnumerable
可以逐块处理数据,而不是一次性加载所有数据到内存。这在处理大型文件或数据库结果集时很有用。
public async IAsyncEnumerable<int> GetLargeDataAsync() { // 模拟从数据库逐行读取数据 for (int i = 0; i < 10000; i++) { await Task.Delay(10); yield return i; } }
- 消费异步流时:
public async Task ProcessDataAsync() { await foreach (var item in GetLargeDataAsync()) { // 处理item Console.WriteLine(item); } }
- 对于处理大量数据的异步操作,使用
使用 SemaphoreSlim
限制并发数
SemaphoreSlim
可以用来限制并发操作的数量。例如,假设一个应用程序需要从多个 API 获取数据,但每个 API 有并发限制,我们可以使用 SemaphoreSlim
来控制并发请求数。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class Program
{
private static SemaphoreSlim _semaphore = new SemaphoreSlim(5, 5); // 初始化并发数为5
static async Task Main()
{
var tasks = new List<Task>();
for (int i = 0; i < 20; i++)
{
tasks.Add(FetchDataAsync(i));
}
await Task.WhenAll(tasks);
_semaphore.Dispose();
}
static async Task FetchDataAsync(int id)
{
await _semaphore.WaitAsync();
try
{
// 模拟API调用
Console.WriteLine($"Task {id} started");
await Task.Delay(1000);
Console.WriteLine($"Task {id} completed");
}
finally
{
_semaphore.Release();
}
}
}
在上述代码中,SemaphoreSlim
初始化允许的并发数为 5。每个 FetchDataAsync
方法在执行实际操作前先调用 WaitAsync
获取信号量,如果信号量已达上限则等待。操作完成后调用 Release
释放信号量,这样其他任务就可以获取信号量继续执行,从而保证系统稳定运行,避免因过多并发导致资源耗尽。