关于 Span 的一切:探索新的 .NET 明星: 3.什么是 Memory<T>,以及为什么你需要它?...

3. 什么是 Memory<T>,以及为什么你需要它?

Span<T> 是包含 ref 字段的仿 ref 类型,并且 ref 字段可以不止于类似数组的开始位置,还可以是中间位置:

var arr = new byte[100];
Span<byte> interiorRef1 = arr.AsSpan(start: 20);
Span<byte> interiorRef2 = new Span<byte>(arr, 20, arr.Length – 20);
Span<byte> interiorRef3 =
  MemoryMarshal.CreateSpan<byte>(arr, ref arr[20], arr.Length – 20);

这些引用被称为中间指针,跟踪它们对于 .NET 运行时的垃圾回收器来说是昂贵的操作。因此,运行时限制这些 ref 只能存在于堆栈,因为它提供了隐式的可能存在的中间指针数量的低限制。

进一步说,如前所展示的那样,Span<T> 比机器的 word 类型尺寸更大,这意味着对 Span 的读、写操作不是原子操作。如果多个线程同时读、写同一个堆中的 Span 的字段,就会带来欲哭无泪的风险。想象一下一个已经初始化的 Span 包含一个有效的引用和一个相关的值为 50 的 _length 字段。一个线程开始在其上写新的 Span,并得到一个新的 _pointer 值。然后,在它设置相关的 _length 为 20 之前,第二个线程读取该 Span,它现在包含新的 _pointer 但是包含了旧的 _length 值。

因此,Span<T> 实例只能在堆栈上存活,而不是堆上。这意味着你不能装箱 Span ( 因此对 Span<T> 使用反射 API,因为这要求装箱操作 )。这意味着你不能在类中定义 Span<T> 字段,甚至是非仿 ref 结构中。它意味着你不能在它们可能隐式成为类中字段的地方使用 Span,例如被捕获到 Lambda 或者在 async 方法中的本地变量,或者迭代器 ( 因为这些 locals 可能最终变成编译器生成的状态机字段 )。这也意味着你不能使用 Span<T> 作为范型参数,因为该类型的实例参数可能最终被装箱,或者存储在堆上 ( 这也是当前没有 where T: ref struct 约束存在的原因 )。

这些限制对很多场景并不重要,特别是在计算密集和同步处理的函数中。但是异步函数就是另外一回事了。在本文开始引用的多数问题是围绕数组来的,数组切片、原生内存等等不管是同步还是异步都存在。然而,如果 Span<T> 不能存储在堆中,也就不能跨异步操作被持久化,答案是什么呢?Memory<T>。

Memory<T> 看起来非常像 ArraySegment<T>:

public readonly struct Memory<T>
{
  private readonly object _object;
  private readonly int _index;
  private readonly int _length;
  ...
}

你可以通过数组来创建 Memory<T>,并像在 Span 中一样进行切片,但是它是非仿 ref 结构,可以保存到堆中。而且,当你希望进行同步操作的时候,你可以通过它获得一个 Span<T>,例如:

static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream)
{
  int bytesRead = await stream.ReadAsync(buffer);
  return Checksum(buffer.Span.Slice(0, bytesRead));
  // Or buffer.Slice(0, bytesRead).Span
}
static int Checksum(Span<byte> buffer) { ... }

与 Span<T> 和 ReadOnlySpan<T> 一样,Memory<T> 也有一个只读的等价物:ReadOnlyMemory<T>。如你所愿,它的 Span 属性返回一个 ReadOnlySpan<T>。图 1 提供了在它们之间进行转换的内建支持的总结。

图 1 在 Span 相关的类型之间进行不分配内存/不复制的转换

来源目标机制
ArraySegment Memory Implicit cast, AsMemory method
ArraySegment ReadOnlyMemory Implicit cast, AsMemory method
ArraySegment ReadOnlySpan Implicit cast, AsSpan method
ArraySegment Span Implicit cast, AsSpan method
ArraySegment T[]Array property
Memory ArraySegment MemoryMarshal.TryGetArray method
Memory ReadOnlyMemory Implicit cast, AsMemory method
Memory Span Span property
ReadOnlyMemory ArraySegment MemoryMarshal.TryGetArray method
ReadOnlyMemory ReadOnlySpan Span property
ReadOnlySpan ref readonly TIndexer get accessor, marshaling methods
Span ReadOnlySpan Implicit cast, AsSpan method
Span ref TIndexer get accessor, marshaling methods
StringReadOnlyMemory AsMemory method
StringReadOnlySpan Implicit cast, AsSpan method
T[]ArraySegment Ctor, Implicit cast
T[]Memory Ctor, Implicit cast, AsMemory method
T[]ReadOnlyMemory Ctor, Implicit cast, AsMemory method
T[]ReadOnlySpan Ctor, Implicit cast, AsSpan method
T[]Span Ctor, Implicit cast, AsSpan method
void*ReadOnlySpan Ctor
void*Span Ctor

你会注意到,Memory<T> 的 _object 字段不是强类型的 T[]; 而是存储了一个对象。这说明了 Memory<T> 可以封装数组之外的数据,例如 System.Buffers.OwnedMemory<T>。OwnedMemory<T> 是一个抽象类,可以用来封装需要拥有自己的生命周期管理的数据,例如从池中获得的内存。这是比本文更为高级的话题,但是它展示了如何使用 Memory<T>,例如,将指针封装到原生内存中。ReadOnlyMemory<char> 也可以用于字符串,如同 ReadOnlySpan<char> 一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值