通过


在 .NET 中使用缓冲区

本文概述了有助于读取跨多个缓冲区运行的数据的类型。 它们主要用于支持 PipeReader 对象。

IBufferWriter<T>

System.Buffers.IBufferWriter<T> 是同步缓冲写入的协定。 在最低级别上,接口:

  • 基本且不难使用。
  • 允许访问 Memory<T>Span<T>。 可以写入 Memory<T>Span<T>,而且你可以确定写入了多少个 T 项。
void WriteHello(IBufferWriter<byte> writer)
{
    // Request at least 5 bytes.
    Span<byte> span = writer.GetSpan(5);
    ReadOnlySpan<char> helloSpan = "Hello".AsSpan();
    int written = Encoding.ASCII.GetBytes(helloSpan, span);

    // Tell the writer how many bytes were written.
    writer.Advance(written);
}

前面的方法:

  • 使用 IBufferWriter<byte>GetSpan(5) 请求至少 5 个字节的缓冲区。
  • 将 ASCII 字符串“Hello”的字节写入返回的 Span<byte> 中。
  • 调用 IBufferWriter<T> 来指示写入缓冲区的字节数。

此写入方法使用 Memory<T> 提供的 /Span<T>IBufferWriter<T> 缓冲区。 或者,Write扩展方法可用于将现有缓冲区复制到 .IBufferWriter<T> Write根据需要执行呼叫GetSpan/Advance工作,因此无需在编写后调用:Advance

void WriteHello(IBufferWriter<byte> writer)
{
    byte[] helloBytes = Encoding.ASCII.GetBytes("Hello");

    // Write helloBytes to the writer. There's no need to call Advance here
    // since Write calls Advance.
    writer.Write(helloBytes);
}

ArrayBufferWriter<T>IBufferWriter<T> 的一种实现,其后盾存储是一个单个连续的数组。

IBufferWriter 常见问题

  • GetSpanGetMemory 返回至少请求内存量的缓冲区。 不要假设确切的缓冲区大小。
  • 不能保证连续调用将返回相同的缓冲区或相同大小的缓冲区。
  • 调用 Advance 以继续写入更多数据后,必须请求新的缓冲区。 在调用Advance之后,之前获取的缓冲区无法再进行写入。

ReadOnlySequence<T>

显示管道中的内存且低于只读内存序列位置的 ReadOnlySequence

ReadOnlySequence<T> 是一个结构,可以表示连续或非连续序列 T。 它通过以下方法进行构造:

  1. 执行 T[] 操作
  2. 执行 ReadOnlyMemory<T> 操作
  3. 一对链接列表节点 ReadOnlySequenceSegment<T> 和索引,表示序列的开始和结束位置。

第三种表示形式最值得关注,因为它对 ReadOnlySequence<T> 上的各种操作有性能影响:

表示形式 操作 复杂性
T[]/ReadOnlyMemory<T> Length O(1)
T[]/ReadOnlyMemory<T> GetPosition(long) O(1)
T[]/ReadOnlyMemory<T> Slice(int, int) O(1)
T[]/ReadOnlyMemory<T> Slice(SequencePosition, SequencePosition) O(1)
ReadOnlySequenceSegment<T> Length O(1)
ReadOnlySequenceSegment<T> GetPosition(long) O(number of segments)
ReadOnlySequenceSegment<T> Slice(int, int) O(number of segments)
ReadOnlySequenceSegment<T> Slice(SequencePosition, SequencePosition) O(1)

由于这种混合表示形式,ReadOnlySequence<T> 将索引显示为SequencePosition而不是整数。 SequencePosition

  • 是一个不透明的值,该值将索引表示为其起源的 ReadOnlySequence<T>
  • 由两个部分组成,一个整数和一个对象。 这两个值表示的内容与实现 ReadOnlySequence<T>相关联。

访问数据

ReadOnlySequence<T> 将数据作为 ReadOnlyMemory<T> 的枚举公开。 可以使用基本 foreach 来枚举每个段:

long FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
    long position = 0;

    foreach (ReadOnlyMemory<byte> segment in buffer)
    {
        ReadOnlySpan<byte> span = segment.Span;
        var index = span.IndexOf(data);
        if (index != -1)
        {
            return position + index;
        }

        position += span.Length;
    }

    return -1;
}

上述方法搜索每个段以获取特定字节。 如果需要跟踪每个段的SequencePositionReadOnlySequence<T>.TryGet更为合适。 下一个示例更改了之前的代码,以返回SequencePosition而不是整数。 返回一个 SequencePosition 好处是允许调用方避免第二次扫描以在特定索引处获取数据。

SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
    SequencePosition position = buffer.Start;
    SequencePosition result = position;

    while (buffer.TryGet(ref position, out ReadOnlyMemory<byte> segment))
    {
        ReadOnlySpan<byte> span = segment.Span;
        var index = span.IndexOf(data);
        if (index != -1)
        {
            return buffer.GetPosition(index, result);
        }

        result = position;
    }
    return null;
}

SequencePositionTryGet 的组合作用类似于枚举器。 在每次迭代开始时,修改位置字段,使其成为ReadOnlySequence<T>中每个段的开始位置。

上述方法作为ReadOnlySequence<T>的扩展方法存在。 PositionOf 可用于简化前面的代码:

SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);

处理 ReadOnlySequence<T>

处理 a ReadOnlySequence<T> 可能很有挑战性,因为数据可以在序列中的多个段之间拆分。 为了获得最佳性能,请将代码拆分为两个路径:

  • 处理单段情况的快速路径。
  • 处理拆分到各个段中的数据的慢速路径。

有几种方法可用于处理多段序列中的数据:

  • 使用SequenceReader<T>
  • 逐段分析数据,同时跟踪已分析的段内的 SequencePosition 和索引。 这可以避免不必要的分配,但可能效率低下,尤其是对于小型缓冲区。
  • 将它 ReadOnlySequence<T> 复制到连续数组,并将其视为单个缓冲区:
    • 如果大小 ReadOnlySequence<T> 较小,则使用 stackalloc 运算符将数据复制到堆栈分配的缓冲区中可能很合理。
    • 使用ReadOnlySequence<T>ArrayPool<T>.Shared复制到池化数组中。
    • 使用 ReadOnlySequence<T>.ToArray()。 不建议在热路径中使用这种方法,因为它会在堆上分配新的 T[]

以下示例演示了一些常见处理 ReadOnlySequence<byte>案例:

处理二进制数据

以下示例从 ReadOnlySequence<byte> 的开头分析 4 字节的大端整数长度。

bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
    // If there's not enough space, the length can't be obtained.
    if (buffer.Length < 4)
    {
        length = 0;
        return false;
    }

    // Grab the first 4 bytes of the buffer.
    var lengthSlice = buffer.Slice(buffer.Start, 4);
    if (lengthSlice.IsSingleSegment)
    {
        // Fast path since it's a single segment.
        length = BinaryPrimitives.ReadInt32BigEndian(lengthSlice.First.Span);
    }
    else
    {
        // There are 4 bytes split across multiple segments. Since it's so small, it
        // can be copied to a stack allocated buffer. This avoids a heap allocation.
        Span<byte> stackBuffer = stackalloc byte[4];
        lengthSlice.CopyTo(stackBuffer);
        length = BinaryPrimitives.ReadInt32BigEndian(stackBuffer);
    }

    // Move the buffer 4 bytes ahead.
    buffer = buffer.Slice(lengthSlice.End);

    return true;
}
处理文本数据

下面的示例:

  • \r\n中查找第一个换行符(ReadOnlySequence<byte>),并通过 out“line”参数返回它。
  • 剪裁该行,从输入缓冲区中排除 \r\n
static bool TryParseLine(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> line)
{
    SequencePosition position = buffer.Start;
    SequencePosition previous = position;
    var index = -1;
    line = default;

    while (buffer.TryGet(ref position, out ReadOnlyMemory<byte> segment))
    {
        ReadOnlySpan<byte> span = segment.Span;

        // Look for \r in the current segment.
        index = span.IndexOf((byte)'\r');

        if (index != -1)
        {
            // Check next segment for \n.
            if (index + 1 >= span.Length)
            {
                var next = position;
                if (!buffer.TryGet(ref next, out ReadOnlyMemory<byte> nextSegment))
                {
                    // You're at the end of the sequence.
                    return false;
                }
                else if (nextSegment.Span[0] == (byte)'\n')
                {
                    //  A match was found.
                    break;
                }
            }
            // Check the current segment of \n.
            else if (span[index + 1] == (byte)'\n')
            {
                // It was found.
                break;
            }
        }

        previous = position;
    }

    if (index != -1)
    {
        // Get the position just before the \r\n.
        var delimeter = buffer.GetPosition(index, previous);

        // Slice the line (excluding \r\n).
        line = buffer.Slice(buffer.Start, delimeter);

        // Slice the buffer to get the remaining data after the line.
        buffer = buffer.Slice(buffer.GetPosition(2, delimeter));
        return true;
    }

    return false;
}
空段

ReadOnlySequence<T>中存储空段是有效的。 显式枚举段时,可能会出现空段:

static void EmptySegments()
{
    // This logic creates a ReadOnlySequence<byte> with 4 segments,
    // two of which are empty.
    var first = new BufferSegment(new byte[0]);
    var last = first.Append(new byte[] { 97 })
                    .Append(new byte[0]).Append(new byte[] { 98 });

    // Construct the ReadOnlySequence<byte> from the linked list segments.
    var data = new ReadOnlySequence<byte>(first, 0, last, 1);

    // Slice using numbers.
    var sequence1 = data.Slice(0, 2);

    // Slice using SequencePosition pointing at the empty segment.
    var sequence2 = data.Slice(data.Start, 2);

    Console.WriteLine($"sequence1.Length={sequence1.Length}"); // sequence1.Length=2
    Console.WriteLine($"sequence2.Length={sequence2.Length}"); // sequence2.Length=2

    // sequence1.FirstSpan.Length=1
    Console.WriteLine($"sequence1.FirstSpan.Length={sequence1.FirstSpan.Length}");

    // Slicing using SequencePosition will Slice the ReadOnlySequence<byte> directly
    // on the empty segment!
    // sequence2.FirstSpan.Length=0
    Console.WriteLine($"sequence2.FirstSpan.Length={sequence2.FirstSpan.Length}");

    // The following code prints 0, 1, 0, 1.
    SequencePosition position = data.Start;
    while (data.TryGet(ref position, out ReadOnlyMemory<byte> memory))
    {
        Console.WriteLine(memory.Length);
    }
}

class BufferSegment : ReadOnlySequenceSegment<byte>
{
    public BufferSegment(Memory<byte> memory)
    {
        Memory = memory;
    }

    public BufferSegment Append(Memory<byte> memory)
    {
        var segment = new BufferSegment(memory)
        {
            RunningIndex = RunningIndex + Memory.Length
        };
        Next = segment;
        return segment;
    }
}

上述代码创建一个 ReadOnlySequence<byte> 包含空段的段,并显示这些空段如何影响各种 API:

  • 使用ReadOnlySequence<T>.SliceSequencePosition指向空段时,该段将被保留。
  • 包含 int 的 ReadOnlySequence<T>.Slice 会跳过空段。
  • 枚举 ReadOnlySequence<T> 会枚举空段。

ReadOnlySequence<T> 和 SequencePosition 的潜在问题

处理ReadOnlySequence<T>/SequencePosition与正常ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int的情况相比时,会有几个不寻常的结果:

  • SequencePosition 是特定 ReadOnlySequence<T>位置的位置标记,而不是绝对位置。 由于它是相对于特定 ReadOnlySequence<T> 的,因此如果在其起源的 ReadOnlySequence<T> 之外使用,则没有意义。
  • 没有SequencePosition,不能在ReadOnlySequence<T>上执行算术运算。 这意味着,执行 position++ 等基本操作将以 position = ReadOnlySequence<T>.GetPosition(1, position) 的形式写入。
  • GetPosition(long)不支持负索引。 这意味着,如果没有遍历所有段,就无法获取倒数第二个字符。
  • 两个 SequencePosition 无法比较,因此很难:
    • 了解一个位置是否大于或小于另一个位置。
    • 编写一些分析算法。
  • ReadOnlySequence<T> 比对象引用更大,应尽可能使用 inref 进行传递。 通过 ReadOnlySequence<T>in 传递 ref 可减少结构的复制。
  • 空段:
    • ReadOnlySequence<T> 中有效。
    • 使用ReadOnlySequence<T>.TryGet方法进行迭代时,可能会出现。
    • 可能会在结合使用 ReadOnlySequence<T>.Slice() 方法与 SequencePosition 对象来对序列进行切片时出现。

SequenceReader<T>

SequenceReader<T>:

  • 在 .NET Core 3.0 中引入了一种新类型,用于简化对 ReadOnlySequence<T> 的处理。
  • 统一单个段 ReadOnlySequence<T> 和多段 ReadOnlySequence<T>之间的差异。
  • 提供用于读取二进制数据和文本数据(bytechar)的帮助程序,这些数据可能会跨段拆分,也可能不会。

有用于处理二进制数据和带分隔符的数据的内置方法。 以下部分演示了这些相同方法结合SequenceReader<T>后的效果:

访问数据

SequenceReader<T> 具有直接枚举 ReadOnlySequence<T> 内部数据的方法。 以下代码是一次处理一个 ReadOnlySequence<byte>byte 的示例:

while (reader.TryRead(out byte b))
{
    Process(b);
}

CurrentSpan 公开了当前段的 Span,这类似于在方法中手动完成的操作。

使用位置

以下代码是使用 FindIndexOf 实现 SequenceReader<T> 的示例:

SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
    var reader = new SequenceReader<byte>(buffer);

    while (!reader.End)
    {
        // Search for the byte in the current span.
        var index = reader.CurrentSpan.IndexOf(data);
        if (index != -1)
        {
            // It was found, so advance to the position.
            reader.Advance(index);

            return reader.Position;
        }
        // Skip the current segment since there's nothing in it.
        reader.Advance(reader.CurrentSpan.Length);
    }

    return null;
}

处理二进制数据

以下示例从 ReadOnlySequence<byte> 的开头分析 4 字节的大端整数长度。

bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
    var reader = new SequenceReader<byte>(buffer);
    return reader.TryReadBigEndian(out length);
}

处理文本数据

static ReadOnlySpan<byte> NewLine => new byte[] { (byte)'\r', (byte)'\n' };

static bool TryParseLine(ref ReadOnlySequence<byte> buffer,
                         out ReadOnlySequence<byte> line)
{
    var reader = new SequenceReader<byte>(buffer);

    if (reader.TryReadTo(out line, NewLine))
    {
        buffer = buffer.Slice(reader.Position);

        return true;
    }

    line = default;
    return false;
}

SequenceReader<T> 常见问题

  • 由于 SequenceReader<T> 是可变结构,因此应始终通过 引用传递它。
  • SequenceReader<T>一个 ref 结构 ,因此它只能在同步方法中使用,并且不能存储在字段中。 有关详细信息,请参阅避免分配
  • SequenceReader<T> 已进行了优化,可用作只进读取器。 Rewind 适用于无法使用其他 ReadAPI PeekIsNext API 解决的小型备份。