这些方法包括:
-
Span<T> 类型,跨度类型也不能在异步操作中使用。
-
Span<T> 结构的不可变版本。
-
Span<T> 限制。
-
Memory<T> 结构的不可变版本。
-
MemoryPool<T>.Dispose() 将其释放回池中。
-
System.Buffers.IMemoryOwner<T>,表示内存块的所有者并控制其生存期管理。
-
MemoryManager<T> 适用于高级方案。
-
ArraySegment<T>,从特定索引开始的特定数量数组元素的包装器。
-
Memory<T> 块的扩展方法集合。
所有者、使用者和生存期管理
下面介绍三个核心概念:
-
所有权也可以转让;组件 A 可以将缓冲区的控制权转让给组件 B,此时组件 A 就无法再使用该缓冲区,组件 B 将负责在不再使用缓冲区时将其销毁。
-
缓冲区的当前使用者不一定是缓冲区的所有者。
-
租用是允许特定组件成为缓冲区使用者的时长。没有用于租用管理的 API;“租用”是概念性内容
.NET Core 支持以下两种所有权模型:
-
缓冲区在其整个生存期内拥有单个所有者。
-
该所有者可以反过来将所有权转让给其他组件等。
Memory<T> 缓冲区的所有权。
using System; using System.Buffers; class Example { static void Main() { using (IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent()) { Console.Write("Enter a number: "); try { var value = Int32.Parse(Console.ReadLine()); var memory = owner.Memory; WriteInt32ToBuffer(value, memory); DisplayBufferToConsole(memory.Slice(0, value.ToString().Length)); } catch (FormatException) { Console.WriteLine("You did not enter a valid number."); } catch (OverflowException) { Console.WriteLine($"You entered a number less than {Int32.MinValue:N0} or greater than {Int32.MaxValue:N0}."); } } } static void WriteInt32ToBuffer(int value, Memory<char> buffer) { var strValue = value.ToString(); var span = buffer.Slice(0, strValue.Length).Span; strValue.AsSpan().CopyTo(span); } static void DisplayBufferToConsole(Memory<char> buffer) => Console.WriteLine($"Contents of the buffer: '{buffer}'"); }
在此代码中:
-
IMemoryOwner<T> 实例的引用,因此
Main方法是缓冲区的所有者。 -
并且它们一次仅使用一个。
ReadOnlyMemory<T> 的参数。
可以通过以下方式达到此目的:
-
Memory<T> 构造函数之一,传入
T[],如下面的示例所示。 -
String.AsMemory 扩展方法以生成
ReadOnlyMemory<char>实例。
using System; class Example { static void Main() { Memory<char> memory = new char[64]; Console.Write("Enter a number: "); var value = Int32.Parse(Console.ReadLine()); WriteInt32ToBuffer(value, memory); DisplayBufferToConsole(memory); } static void WriteInt32ToBuffer(int value, Memory<char> buffer) { var strValue = value.ToString(); strValue.AsSpan().CopyTo(buffer.Slice(0, strValue.Length).Span); } static void DisplayBufferToConsole(Memory<char> buffer) => Console.WriteLine($"Contents of the buffer: '{buffer}'"); }
(或者,也可以假设运行时的垃圾回收器拥有缓冲区,而所有方法仅使用缓冲区。)
使用准则
Span<T> 的准则。
- 规则 1:对于同步 API,如有可能,请使用 Span<T>(而不是 Memory<T>)作为参数。
Span<T> 参数调用你的方法。
你将自动进行编译时检查,以确保不尝试访问方法租用之外的缓冲区(后续部分将对此进行详细介绍)。
- 规则 2:如果缓冲区应为只读,则使用 ReadOnlySpan<T> 或 ReadOnlyMemory<T>。
- 规则 3:如果方法接受 Memory<T> 并返回
void,则在方法返回之后不得使用 Memory<T> 实例。
Memory<T> 实例的租用将在进入该方法时开始,并在退出该方法时结束。
using System; using System.Buffers; public class Example { // implementation provided by third party static extern void Log(ReadOnlyMemory<char> message); // user code public static void Main() { using (var owner = MemoryPool<char>.Shared.Rent()) { var memory = owner.Memory; var span = memory.Span; while (true) { int value = Int32.Parse(Console.ReadLine()); if (value < 0) return; int numCharsWritten = ToBuffer(value, span); Log(memory.Slice(0, numCharsWritten)); } } } private static int ToBuffer(int value, Span<char> span) { string strValue = value.ToString(); int length = strValue.Length; strValue.AsSpan().CopyTo(span.Slice(0, length)); return length; } }
如果 Log 是完全同步的方法,则此代码将按预期运行,因为在任何给定时间只有一个活动的内存实例使用者。
但如果是异步方法,如下例子,则导致访问已经释放的缓冲区
// !!! INCORRECT IMPLEMENTATION !!! static void Log(ReadOnlyMemory<char> message) { // Run in background so that we don't block the main thread while performing IO. Task.Run(() => { StreamWriter sw = File.AppendText(@".\input-numbers.dat"); sw.WriteLine(message); }); }
有多种方法可解决此问题:
-
Task,而不是
void。// An acceptable implementation. static Task Log(ReadOnlyMemory<char> message) { // Run in the background so that we don't block the main thread while performing IO. return Task.Run(() => { StreamWriter sw = File.AppendText(@".\input-numbers.dat"); sw.WriteLine(message); sw.Flush(); }); }
-
可以改为按如下所示实现
Log:// An acceptable implementation. static void Log(ReadOnlyMemory<char> message) { string defensiveCopy = message.ToString(); // Run in the background so that we don't block the main thread while performing IO. Task.Run(() => { StreamWriter sw = File.AppendText(@".\input-numbers.dat"); sw.WriteLine(defensiveCopy); sw.Flush(); }); }
-
规则 7:如果具有 IMemoryOwner<T> 引用,则必须在某些时候对其进行处理或转让其所有权。
MemoryPool<T>.Dispose(稍后将对此进行详细介绍)。
Dispose 方法失败可能会导致非托管内存泄漏或其他性能降低。
IMemoryOwner<T> 的所有者,并负责在完成后处理实例。
- 规则 8:如果 API 接口中具有 IMemoryOwner<T> 参数,即表示你接受该实例的所有权。
组件将负责根据规则 7 进行正确处理。
IMemoryOwner<T> 实例的所有权转让给其他组件的任何组件应不再使用该实例。
MemoryPool<T>.Dispose。