这些方法包括:

  • 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);
    });
}

有多种方法可解决此问题:

  1. 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();
        });
    }
  2. 可以改为按如下所示实现 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。

 

 

相关文章: