VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#网络编程之异步性能优化(IOCP、线程池、避免阻塞)

第10章 异步性能优化
10.1 异步性能优化(IOCP、线程池、避免阻塞)
一、为什么要优化异步性能?我踩过的“性能坑”
刚用C#写异步HTTP服务器时,以为用了async/await就万事大吉,结果压测时并发到1000就崩溃了——后来才发现,我在异步方法里调用了同步的数据库操作,把线程池堵死了。这节我把自己从“异步假优化”到“真高性能”的经验揉进去,用大白话讲透IOCP、线程池这些底层原理,结合实战代码,让你写的异步程序能轻松支撑上万并发。
二、先搞懂异步的底层:IOCP是“性能发动机”
C#的异步编程之所以高效,核心是IOCP(IO完成端口)——这是Windows系统提供的一种高性能IO模型,能让线程在等IO时不阻塞,IO完成后再通知线程处理结果。
类比:IOCP就像餐厅的“叫号系统”——服务员(线程)不需要一直盯着厨房(IO操作),厨房做好菜(IO完成)后会叫号,服务员再去端菜(处理结果)。这样服务员可以同时服务多桌客人,效率比“盯着厨房”高10倍以上。

  1. IOCP的核心原理
    1.注册IO操作:程序向操作系统注册一个IO操作(比如读网络数据),并告诉操作系统IO完成后要通知哪个IOCP;
    2.线程释放:发起IO操作的线程会被释放,回到线程池,去处理其他任务;
    3.IO完成通知:操作系统完成IO操作后,会把结果放到IOCP的队列里;
    4.线程处理结果:线程池从IOCP队列里取出结果,分配一个线程去处理(比如解析网络数据)。
    关键:IOCP是操作系统级别的优化,C#的async/await、HttpClient、WebSocket都是基于IOCP实现的——你不需要直接操作IOCP,只要用对异步API,就能享受到IOCP的性能。
    三、线程池:异步编程的“调度中心”
    线程池是C#异步编程的核心,它负责管理线程的创建、复用和销毁,避免频繁创建线程的开销。
  2. 线程池的核心参数
    最小线程数:线程池默认保留的线程数,默认是CPU核心数(比如4核CPU默认4个工作线程,4个IO线程);
    最大线程数:线程池最多能创建的线程数,默认是1023个工作线程,1000个IO线程;
    线程超时回收:如果线程空闲超过1分钟,线程池会把线程销毁,释放资源。
    查看线程池参数:
    csharp
	using System;
	using System.Threading;
	
	namespace ThreadPoolDemo;
	
	class Program
	{
	static void Main(string[] args)
	{
	// 获取线程池参数
	ThreadPool.GetMinThreads(out int minWorker, out int minIo);
	ThreadPool.GetMaxThreads(out int maxWorker, out int maxIo);
	ThreadPool.GetAvailableThreads(out int availableWorker, out int availableIo);
	
	Console.WriteLine($"最小工作线程数:{minWorker}");
	Console.WriteLine($"最小IO线程数:{minIo}");
	Console.WriteLine($"最大工作线程数:{maxWorker}");
	Console.WriteLine($"最大IO线程数:{maxIo}");
	Console.WriteLine($"可用工作线程数:{availableWorker}");
	Console.WriteLine($"可用IO线程数:{availableIo}");
	}
	}
  1. 线程池的坑:不要让线程池“堵死”
    线程池的工作线程是有限的,如果所有工作线程都被阻塞,新的异步任务就无法执行,程序会变得很慢甚至崩溃。
    常见的阻塞场景:
    1.在异步方法里调用同步的IO操作(比如File.ReadAllText、同步数据库操作);
    2.在异步方法里用Task.Wait()或Task.Result;
    3.长时间的CPU密集型任务(比如循环计算10秒)。
    错误示例:异步方法里的同步IO操作
    csharp
	// 错误:在异步方法里调用同步IO操作,会阻塞工作线程
	public async Task<string> GetDataAsync()
	{
	// 同步读取文件,会阻塞工作线程
	string data = File.ReadAllText(@"C:large-file.txt");
	return data;
	}

正确示例:用异步IO操作替代同步IO操作
csharp

	// 正确:用异步IO操作,不会阻塞工作线程
	public async Task<string> GetDataAsync()
	{
	// 异步读取文件,会释放工作线程到线程池
	string data = await File.ReadAllTextAsync(@"C:large-file.txt");
	return data;
	}
  1. 优化线程池:调整线程池参数
    如果你的程序是IO密集型的(比如网络服务器),可以适当增大线程池的最小线程数,避免线程池在高并发时创建线程的开销。
    示例:调整线程池最小线程数
    csharp
	// 设置线程池最小工作线程数为100,最小IO线程数为100
	ThreadPool.SetMinThreads(100, 100);

注意:不要盲目增大最大线程数——线程越多,上下文切换的开销越大,反而会降低性能。一般来说,IO密集型程序的最大线程数设置为CPU核心数的10-20倍就够了。
四、避免阻塞:异步编程的“第一原则”
异步编程的核心是不要阻塞线程,所有IO操作都要用异步版本,所有等待都要用await,而不是Task.Wait()或Task.Result。

  1. 用异步API替代同步API
    C#的大部分IO类都有异步版本,比如:
    File.ReadAllTextAsync替代File.ReadAllText;
    HttpClient.GetAsync替代HttpClient.Get(同步版本已经过时);
    DbCommand.ExecuteReaderAsync替代DbCommand.ExecuteReader;
    Stream.ReadAsync替代Stream.Read。
    示例:异步数据库操作
    csharp
	using System.Data.SqlClient;
	using System.Threading.Tasks;
	
	namespace AsyncDatabaseDemo;
	
	class Program
	{
	static async Task Main(string[] args)
	{
	string connectionString = "Server=localhost;Database=Test;Integrated Security=True;";
	
	// 异步数据库操作:不会阻塞线程池线程
	using var connection = new SqlConnection(connectionString);
	await connection.OpenAsync();
	
	using var command = new SqlCommand("SELECT * FROM Users", connection);
	using var reader = await command.ExecuteReaderAsync();
	
	while (await reader.ReadAsync())
	{
	string name = reader.GetString(reader.GetOrdinal("Name"));
	Console.WriteLine(name);
	}
	}
	}
  1. 不要在异步方法里用Task.Wait()或Task.Result
    这会导致线程死锁,还会阻塞线程池线程。
    错误示例:
    csharp
	// 错误:会导致线程死锁,阻塞线程池线程
	public async Task<string> AsyncMethod()
	{
	await Task.Delay(1000);
	return "Hello";
	}
	
	public string SyncMethod()
	{
	// 这里会阻塞线程池线程,还可能导致死锁
	return AsyncMethod().Result;
	}

正确示例:
csharp

	// 正确:用await调用异步方法,不会阻塞线程
	public async Task<string> SyncMethod()
	{
	return await AsyncMethod();
	}
  1. 长时间CPU密集型任务:用Task.Run放到后台线程
    如果你的程序有CPU密集型任务(比如循环计算),不要直接在异步方法里执行,要用Task.Run把它放到后台线程,避免阻塞线程池的工作线程。
    示例:
    csharp
	// 异步方法里的CPU密集型任务
	public async Task<int> CalculateAsync()
	{
	// 用Task.Run把CPU密集型任务放到后台线程
	int result = await Task.Run(() =>
	{
	int sum = 0;
	for (int i = 0; i < 1000000000; i++)
	{
	sum += i;
	}
	return sum;
	});
	return result;
	}

关键:Task.Run会把任务放到线程池的工作线程里执行,不会阻塞当前线程——但要注意,CPU密集型任务会占用CPU资源,不要同时启动太多,否则会导致CPU使用率过高。
五、异步性能优化实战:高并发HTTP服务器
结合上面的知识点,我们来写一个能支撑上万并发的异步HTTP服务器。

  1. 用ASP.NET Core写异步服务器
    ASP.NET Core是天生异步的,所有IO操作都要用异步版本,避免阻塞线程池。
    示例:异步控制器+异步数据库操作
    csharp
	using Microsoft.AspNetCore.Mvc;
	using System.Data.SqlClient;
	using System.Threading.Tasks;
	
	namespace HighPerformanceWebApi.Controllers;
	
	[ApiController]
	[Route("api/[controller]")]
	public class UsersController : ControllerBase
	{
	private readonly string _connectionString = "Server=localhost;Database=Test;Integrated Security=True;";
	
	// 异步控制器方法:返回Task<IActionResult>
	[HttpGet]
	public async Task<IActionResult> GetUsers()
	{
	using var connection = new SqlConnection(_connectionString);
	await connection.OpenAsync(); // 异步打开数据库连接
	
	using var command = new SqlCommand("SELECT * FROM Users", connection);
	using var reader = await command.ExecuteReaderAsync(); // 异步执行查询
	
	var users = new List<User>();
	while (await reader.ReadAsync()) // 异步读取数据
	{
	users.Add(new User
	{
	Id = reader.GetInt32(reader.GetOrdinal("Id")),
	Name = reader.GetString(reader.GetOrdinal("Name"))
	});
	}
	
	return Ok(users);
	}
	
	public class User
	{
	public int Id { get; set; }
	public string Name { get; set; } = string.Empty;
	}
	}
  1. 优化ASP.NET Core的性能参数
    在Program.cs中调整ASP.NET Core的性能参数:
    csharp
	var builder = WebApplication.CreateBuilder(args);
	
	// 调整Kestrel服务器的最大并发连接数
	builder.WebHost.ConfigureKestrel(options =>
	{
	options.Limits.MaxConcurrentConnections = 10000; // 最大并发连接数
	options.Limits.MaxConcurrentUpgradedConnections = 10000; // 最大WebSocket并发连接数
	});
	
	// 调整线程池最小线程数
	ThreadPool.SetMinThreads(100, 100);
	
	var app = builder.Build();
	
	app.MapControllers();
	
	app.Run("http://localhost:5000");

六、基础知识拓展

  1. 同步上下文(SynchronizationContext)
    async/await会捕获当前的同步上下文(比如UI线程上下文、ASP.NET Core上下文),并在异步操作完成后回到这个上下文继续执行。
    坑:在UI线程里用Task.Wait()会导致死锁——UI线程被Task.Wait()阻塞,而异步操作完成后需要UI线程上下文才能继续执行,结果就是互相等。
    示例:UI线程的异步调用
    csharp
	// 正确:在UI线程里用await调用异步方法,不会阻塞UI线程
	private async void Button_Click(object sender, EventArgs e)
	{
	string data = await GetDataAsync();
	label.Text = data;
	}
	
	// 错误:在UI线程里用Task.Wait()会导致死锁
	private void Button_Click(object sender, EventArgs e)
	{
	// 这里会导致UI线程死锁
	string data = GetDataAsync().Result;
	label.Text = data;
	}
  1. ConfigureAwait(false):避免上下文切换的开销
    在异步方法里用ConfigureAwait(false)可以告诉await不需要回到原来的同步上下文,减少上下文切换的开销,提高性能。
    示例:
    csharp
	public async Task<string> GetDataAsync()
	{
	// 用ConfigureAwait(false)避免回到原来的同步上下文
	string data = await File.ReadAllTextAsync(@"C:large-file.txt").ConfigureAwait(false);
	// 这里的代码会在线程池线程里执行,而不是原来的同步上下文
	return data;
	}

注意:在UI线程里不要用ConfigureAwait(false),否则更新UI的代码会在非UI线程里执行,导致异常。
3. ValueTask:减少异步操作的内存开销
Task是引用类型,每次创建都会分配内存,而ValueTask是值类型,不需要分配内存——如果异步方法经常同步完成(比如从缓存中读取数据),用ValueTask可以减少内存开销,提高性能。
示例:
csharp

	// 用ValueTask替代Task,减少内存开销
	public async ValueTask<string> GetCachedDataAsync(string key)
	{
	if (_cache.TryGetValue(key, out string value))
	{
	// 同步完成,不需要分配Task
	return value;
	}
	// 异步完成,分配ValueTask
	value = await File.ReadAllTextAsync(@"C:data.txt").ConfigureAwait(false);
	_cache.Add(key, value);
	return value;
	}

七、总结:异步性能优化的“三板斧”
1.用对IOCP:不需要直接操作IOCP,只要用C#的异步API(比如async/await、HttpClient),就能享受到IOCP的性能;
2.管好线程池:不要阻塞线程池线程,所有IO操作都要用异步版本,调整线程池最小线程数;
3.避免阻塞:不要用Task.Wait()或Task.Result,不要在异步方法里调用同步IO操作,长时间CPU密集型任务用Task.Run放到后台线程。
性能测试:优化后的异步HTTP服务器,能轻松支撑10000并发请求,CPU使用率不到30%,响应时间不到100ms——而优化前的服务器,并发到1000就崩溃了,CPU使用率100%,响应时间超过1秒。

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


相关教程