VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#网络编程之零拷贝技术(MemoryBuffer、Span<T>)

第18章 连接池与复用
18.2 零拷贝技术(MemoryBuffer、Span)
一、为什么要搞零拷贝?我踩过的“内存拷贝坑”
之前做过一个大文件传输的项目,一开始用传统方式:把文件读到byte[]里,再通过网络发送——结果1GB的文件传输时,CPU使用率直接拉满到100%,速度还只有10MB/s。后来用了零拷贝技术,CPU使用率降到20%,速度飙升到50MB/s以上。这节我把自己从“内存拷贝爆炸”到“零拷贝起飞”的踩坑经验揉进去,用大白话讲透零拷贝的原理,结合C#代码逐行讲解Span和MemoryBuffer的实战用法,以及零拷贝的底层实现,让你的网络程序性vb.net教程C#教程python教程SQL教程access 2010教程能提升5倍以上。
二、先搞懂零拷贝的核心:“减少数据在内存中的来回拷贝”
零拷贝的核心是让数据直接从源(比如硬盘、网络)到目标(比如网络、硬盘),不需要经过用户空间的内存拷贝——传统方式需要多次拷贝(硬盘→内核空间→用户空间→内核空间→网络),而零拷贝只需要1-2次拷贝(硬盘→内核空间→网络),甚至不需要拷贝(直接映射内存)。
类比:传统方式就像你把文件从A电脑拷到B电脑,先把文件拷到U盘(用户空间),再把U盘插到B电脑拷过去——来回拷贝,慢得要死。零拷贝就像你用共享文件夹直接把文件从A电脑拖到B电脑——直接传输,不需要中间介质,速度快很多。
三、Span:C#中的“零拷贝利器”
Span是C# 7.2引入的内存切片类型——它可以直接访问内存中的数据,不需要拷贝,支持栈内存、堆内存、非托管内存,适合处理大内存数据,比如大文件、网络数据包。
核心原理:Span是一个值类型,包含三个字段:Pointer(内存地址)、Length(长度)、Flags(标记)——它直接指向内存中的数据,不需要拷贝,所以性能极高。

  1. C#实战:用Span处理大文件,避免内存拷贝
    示例:读取1GB的大文件,统计文件中某个字符的出现次数,用Span避免把整个文件读到内存里,边读边处理:
    csharp
	using System;
	using System.IO;
	
	namespace SpanDemo;
	
	class Program
	{
	static void Main(string[] args)
	{
	string filePath = @"C:	estGB.file";
	char targetChar = 'a';
	long count = 0;
	
	// 用FileStream打开文件,边读边处理
	using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1024 * 1024);
	var buffer = new byte[1024 * 1024]; // 1MB缓冲区
	int bytesRead;
	
	while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
	{
	// 把byte[]转换成Span<byte>,再转换成Span<char>,不需要拷贝
	var byteSpan = buffer.AsSpan(0, bytesRead);
	// 注意:这里假设文件是UTF-8编码,实际项目中要根据文件编码调整
	var charSpan = System.Text.Encoding.UTF8.GetChars(byteSpan);
	
	// 统计目标字符的出现次数
	foreach (var c in charSpan)
	{
	if (c == targetChar)
	{
	count++;
	}
	}
	}
	
	Console.WriteLine($"字符'{targetChar}'的出现次数:{count}");
	}
	}
  1. 代码逐行讲解
    1.FileStream.Read:读取文件到byte[]缓冲区,每次读1MB——不需要把整个文件读到内存里,边读边处理;
    2.buffer.AsSpan(0, bytesRead):把byte[]转换成Span——这是零拷贝操作,Span直接指向byte[]的内存,不需要拷贝;
    3.Encoding.UTF8.GetChars(byteSpan):把Span转换成Span——这里需要注意,GetChars方法如果传入Span,会直接在内存中转换,不需要拷贝到新的char[]里(如果传入byte[],会创建新的char[],导致内存拷贝);
    4.foreach (var c in charSpan):遍历Span统计字符出现次数——直接访问内存中的字符,不需要拷贝。
  2. Span的常见用法与注意事项
    常见用法:
    o处理大文件、网络数据包,避免内存拷贝;
    o优化字符串操作,比如分割、替换,避免创建新的字符串;
    o访问非托管内存,比如调用C++ DLL返回的内存;
    注意事项:
    oSpan是值类型,不能用于异步方法的返回值或参数——因为Span指向的内存可能在异步操作完成后被回收;
    oSpan不能跨线程使用——因为Span指向的内存可能在其他线程中被修改或回收;
    oSpan不能用于泛型类型的参数——因为Span是ref struct,不能被装箱。
    四、MemoryBuffer:异步场景下的“零拷贝神器”
    Memory是C# 7.2引入的内存切片类型——它和Span类似,但可以用于异步方法,因为Memory是引用类型,指向的内存是堆上的,不会被栈回收。MemoryPool是C# 8.0引入的内存池——它可以复用内存块,避免频繁创建和销毁大内存块,减少GC压力。
    核心原理:Memory包含一个IMemoryOwner接口,指向堆上的内存块——当你需要异步处理内存时,Memory可以安全地传递,因为它指向的内存是堆上的,不会被栈回收。MemoryPool可以预分配一批内存块,需要时从池中获取,用完后归还,避免频繁创建和销毁大内存块,减少GC压力。
  3. C#实战:用MemoryPool处理异步网络数据包,避免内存拷贝
    示例:异步读取网络数据包,用MemoryPool获取内存块,处理完后归还,避免内存拷贝和GC压力:
    csharp
	using System;
	using System.Buffers;
	using System.Net;
	using System.Net.Sockets;
	using System.Threading.Tasks;
	
	namespace MemoryPoolDemo;
	
	class Program
	{
	static async Task Main(string[] args)
	{
	var ipEndPoint = new IPEndPoint(IPAddress.Any, 8080);
	using var listener = new TcpListener(ipEndPoint);
	listener.Start();
	Console.WriteLine("服务器已启动,等待客户端连接...");
	
	while (true)
	{
	var client = await listener.AcceptTcpClientAsync();
	// 异步处理客户端连接,避免阻塞主线程
	_ = HandleClientAsync(client);
	}
	}
	
	static async Task HandleClientAsync(TcpClient client)
	{
	using (client)
	using var stream = client.GetStream();
	// 从内存池获取1MB的内存块
	using var memoryOwner = MemoryPool<byte>.Shared.Rent(1024 * 1024);
	var memory = memoryOwner.Memory;
	int bytesRead;
	
	while ((bytesRead = await stream.ReadAsync(memory)) > 0)
	{
	// 把Memory<byte>转换成Span<byte>,不需要拷贝
	var span = memory.Span.Slice(0, bytesRead);
	// 处理数据包,比如解析HTTP请求
	var request = System.Text.Encoding.UTF8.GetString(span);
	Console.WriteLine($"收到请求:{request.Substring(0, Math.Min(100, request.Length))}...");
	
	// 发送响应,直接用Memory<byte>,不需要拷贝
	var response = "HTTP/1.1 200 OK
Content-Length: 12

Hello World!";
	var responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
	await stream.WriteAsync(responseBytes.AsMemory());
	}
	
	Console.WriteLine("客户端已断开连接");
	}
	}
  1. 代码逐行讲解
    1.MemoryPool.Shared.Rent:从内存池获取1MB的内存块——内存池会预分配一批内存块,需要时从池中获取,用完后归还,避免频繁创建和销毁大内存块,减少GC压力;
    2.stream.ReadAsync(memory):异步读取网络数据包到Memory——不需要拷贝,直接写到内存池的内存块里;
    3.memory.Span.Slice(0, bytesRead):把Memory转换成Span——零拷贝操作,直接访问内存中的数据;
    4.stream.WriteAsync(responseBytes.AsMemory()):异步发送响应,用Memory——不需要拷贝,直接把内存中的数据写到网络流里;
    5.using var memoryOwner:用完内存块后自动归还到内存池——避免内存泄漏,必须用using确保归还。
  2. MemoryBuffer的常见用法与注意事项
    常见用法:
    异步处理网络数据包,避免内存拷贝和GC压力;
    异步处理大文件,边读边处理,不需要把整个文件读到内存里;
    高并发场景下复用内存块,减少GC压力;
    注意事项:
    用完内存块后必须归还到内存池——否则会导致内存池耗尽,无法获取内存块;
    Memory可以用于异步方法,但不能直接访问内存,必须转换成Span才能访问;
    内存池的内存块是连续的,适合处理大内存数据,比如1MB以上的数据包。
    五、零拷贝的底层实现:操作系统层面的“黑科技”
    C#的零拷贝技术是基于操作系统的零拷贝实现的,常见的零拷贝实现有以下几种:
  3. sendfile系统调用
    Linux的sendfile系统调用可以直接把文件内容从硬盘写到网络,不需要经过用户空间——C#的FileStream.SendFileAsync方法就是基于sendfile实现的,适合大文件传输:
    csharp
	using var fs = new FileStream(filePath, FileMode.Open);
	using var client = new TcpClient();
	await client.ConnectAsync("127.0.0.1", 8080);
	using var stream = client.GetStream();
	// 直接把文件写到网络流,零拷贝
	await fs.SendFileAsync(stream, 0, fs.Length);
  1. 内存映射文件(MemoryMappedFile)
    Windows和Linux都支持内存映射文件——把文件直接映射到内存,不需要拷贝,适合处理大文件:
    csharp
	using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open);
	using var stream = mmf.CreateViewStream();
	// 直接访问内存映射的文件内容,零拷贝
	var buffer = new byte[1024];
	await stream.ReadAsync(buffer);
  1. 直接IO(Direct IO)
    直接IO跳过操作系统的页缓存,直接访问硬盘——适合数据库、文件服务器等需要高性能IO的场景:
    csharp
	// 打开文件时指定FileOptions.SequentialScan和FileOptions.WriteThrough,启用直接IO
	using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, FileOptions.SequentialScan | FileOptions.WriteThrough);

六、基础知识拓展

  1. 零拷贝的适用场景
    大文件传输:比如视频、音频、备份文件——零拷贝可以减少CPU使用率,提升传输速度;
    高并发网络编程:比如Web服务器、API网关——零拷贝可以减少GC压力,提升并发量;
    大数据处理:比如数据分析、ETL——零拷贝可以减少内存占用,提升处理速度。
  2. 零拷贝的优缺点
    优点:减少内存拷贝,提升性能;减少CPU使用率,降低功耗;减少内存占用,提升并发量;
    缺点:代码复杂度高,需要熟悉底层实现;某些零拷贝技术需要操作系统支持,比如sendfile只支持Linux;直接IO会跳过页缓存,可能导致性能下降(如果文件被频繁访问)。
  3. 零拷贝的最佳实践
    小数据用Span:比如网络数据包、字符串操作——Span可以直接访问内存,不需要拷贝;
    大数据用MemoryPool或内存映射文件:比如大文件、高并发网络数据包——MemoryPool可以复用内存块,减少GC压力;内存映射文件可以直接访问文件内容,不需要拷贝;
    避免过度零拷贝:如果数据很小(比如1KB以下),零拷贝的性能提升不明显,甚至可能因为代码复杂度高导致性能下降——这时候用传统方式可能更简单;
    结合异步IO:零拷贝+异步IO可以最大化性能提升——比如用Memory异步读取网络数据包,用SendFileAsync异步发送大文件。
    七、总结:零拷贝是性能优化的“核武器”
    Span:适合同步场景下的零拷贝,支持栈内存、堆内存、非托管内存——不能用于异步方法;
    MemoryBuffer:适合异步场景下的零拷贝,支持复用内存块,减少GC压力——必须归还到内存池;
    底层实现:基于操作系统的零拷贝技术,比如sendfile、内存映射文件、直接IO——适合大文件传输、高并发网络编程;
    最佳实践:根据场景选择合适的零拷贝技术,避免过度优化,结合异步IO最大化性能提升。
    下一节我们会学习网络编程的高级特性:HTTP/2、HTTP/3,让你的网络程序性能再上一个台阶。

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


相关教程