作者: 大師兄石頭
來源: https://bigbrotherstone.cnblogs.com/
1. 前言
2. 簡介
3. Memory<T>和Span<T>使用準則
1. 前言此文章是官方文檔的翻譯,由於官方文檔中文版是機器翻譯的,有些部分有疏漏和錯誤,所以本人進行了翻譯供大家學習,如有問題歡迎指正。
參考資料:
memory-and-spans --- Microsoft
.NET 包含多個相互關聯的類型,它們表示任意內存的連續的強類型區域。這些方法包括:
System.Span<T>、System.Memory<T> 及其對應的只讀類型被設計為:
備註:
對於早期框架,Span<T> 和 Memory<T> 在 System.Memory NuGet 包中提供。
使用 memory 和 span
3. Memory<T>和Span<T>使用準則Span<T> 和 Memory<T> 都是可用於 pipeline 的結構化數據的緩衝區。
3.1. 所有者, 消費者和生命周期管理由於可以在各個 API 之間傳送緩衝區,以及由於緩衝區有時可以從多個線程進行訪問,因此請務必考慮生命周期管理。下面介紹三個核心概念:
以下偽代碼示例闡釋了這三個概念。它包括:
實例化類型為 Char 的 Memory<T> 緩衝區的
調用 WriteInt32ToBuffer 方法以將整數的字符串表示形式寫入緩衝區
然後調用 DisplayBufferToConsole 方法以顯示緩衝區的值。
using System;
class Program
{
// Write 'value' as a human-readable string to the output buffer.
void WriteInt32ToBuffer(int value, Buffer buffer);
// Display the contents of the buffer to the console.
void DisplayBufferToConsole(Buffer buffer);
// Application code
static void Main()
{
var buffer = CreateBuffer();
try
{
int value = Int32.Parse(Console.ReadLine());
WriteInt32ToBuffer(value, buffer);
DisplayBufferToConsole(buffer);
}
finally
{
buffer.Destroy();
}
}
}
所有者
消費者
先是 WriteInt32ToBuffer ,然後是 DisplayBufferToConsole
WriteInt32ToBuffer 和 DisplayBufferToConsole
一次只能有一個消費者
這兩個消費者都不擁有緩衝區
此上下文中的「消費者」並不意味著以只讀形式查看緩衝區;如果提供了以讀/寫形式查看緩衝區的權限,則消費者可以像 WriteInt32ToBuffer 那樣修改緩衝區的內容
租約
3.2. Memory<T> 和所有者/消費者模型.NET Core 支持以下兩種所有權模型:
使用 System.Buffers.IMemoryOwner<T> 接口顯式的管理緩衝區的所有權。
IMemoryOwner<T> 支持上述這兩種所有權模型
具有 IMemoryOwner<T> 引用的組件擁有緩衝區
以下示例使用 IMemoryOwner<T> 實例反映 Memory<T> 緩衝區的所有權。
using System;
using System.Buffers;
class Example
{
static void Main()
{
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(owner.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}.");
}
finally {
owner?.Dispose();
}
}
static void WriteInt32ToBuffer(int value, Memory<char> buffer)
{
var strValue = value.ToString();
var span = buffer.Span;
for (int ctr = 0; ctr < strValue.Length; ctr++)
span[ctr] = strValue[ctr];
}
static void DisplayBufferToConsole(Memory<char> buffer) =>
Console.WriteLine($"Contents of the buffer: '{buffer}'");
}
也可以使用 using 編寫此示例:
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}'");
}
在此代碼中:
儘管 WriteInt32ToBuffer 方法用於將數據寫入緩衝區,但 DisplayBufferToConsole 方法並不如此。
3.3. 「缺少所有者」 的Memory<T> 實例無需使用 IMemoryOwner<T> 即可創建 Memory<T> 實例。在這種情況下,緩衝區的所有權是隱式的,並且僅支持單所有者模型。可以通過以下方式達到此目的:
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}'");
}
因為擁有一個內存塊,但打算將其傳遞給多個組件,其中一些組件可能同時在特定的內存塊上運行,所以建立使用Memory<T>和Span<T>的準則是很必要的,因為:
所有者釋放它之後,一個組件還可能會保留對該存儲塊的引用。
兩個組件可能並發的同時在緩衝區上進行操作,從而破壞了緩衝區中的數據。
儘管Span<T>的堆棧分配性質優化了性能,而且使Span<T>成為在內存塊上運行的首選類型,但它也使Span<T>受到一些主要限制
下面介紹成功使用 Memory<T> 及其相關類型的建議。除非另有明確說明,否則適用於 Memory<T> 和 Span<T> 的指南也適用於 ReadOnlyMemory<T> 和 ReadOnlySpan<T> 。
規則 1:對於同步 API,如有可能,請使用 Span<T>(而不是 Memory<T>)作為參數。
Span<T> 比 Memory<T> 更多功能:
可以表示更多種類的連續內存緩衝區
Span<T> 還提供比 Memory<T> 更好的性能
無法進行 Span<T> 到 Memory<T> 的轉換
可以使用 Memory<T>.Span 屬性將 Memory<T> 實例轉換為 Span<T>
使用類型 Span<T>(而不是類型 Memory<T>)作為方法的參數類型還可以幫助你編寫正確的消費方法實現。你將自動進行編譯時檢查,以確保不會企圖訪問此方法租約之外的緩衝區
有時,必須使用 Memory<T> 參數(而不是 Span<T> 參數),即使完全同步也是如此。所依賴的 API 可能僅接受 Memory<T> 參數。這沒有問題,但當使用同步的 Memory<T> 時,應注意權衡利弊
規則 2:如果緩衝區應為只讀,則使用 ReadOnlySpan<T> 或 ReadOnlyMemory<T>
在前面的示例中,DisplayBufferToConsole 方法僅從緩衝區讀取數據;它不修改緩衝區的內容。方法籤名應進行修改如下。
void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);
事實上,如果我們結合 規則1 和 規則2 ,我們可以做得更好,並重寫方法籤名如下:
void DisplayBufferToConsole(ReadOnlySpan<char> buffer);
DisplayBufferToConsole 方法現在幾乎適用於每一個能夠想到的緩衝區類型:
規則 3:如果方法接受 Memory<T> 並返回 void,則該方法的代碼中return之後不得使用 Memory<T> 實例,保證方法結束後對其使用也結束。
這與前面提到的「租約」概念相關。返回 void 的方法對 Memory<T> 實例的租用將在進入該方法時開始,並在退出該方法時結束。請考慮以下示例,該示例會基於控制臺中的輸入在循環中調用 Log。
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 是完全同步的方法,則此代碼將按預期運行,因為在任何給定時間只有一個活躍的內存實例消費者。但是,請想像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);
});
}
在此實現中,Log 違反了租約,因為它在 return 之後仍嘗試在後臺使用 Memory<T> 實例。Main 方法可能會在 Log 嘗試從緩衝區進行讀取時更改緩衝區數據,這可能導致消費者在使用緩存區數據時數據已經被修改。
有多種方法可解決此問題:
Log 方法可以按以下所示,返回 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();
});
}
規則 4:如果方法接受 Memory<T> 並返回某個Task,則在Task轉換為終止狀態之前不得使用 Memory<T> 實例。
這個是 規則3 的異步版本。以下示例是遵守此規則,按上面例子編寫的 Log 方法:
// 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(() => {
string defensiveCopy = message.ToString();
StreamWriter sw = File.AppendText(@".\input-numbers.dat");
sw.WriteLine(defensiveCopy);
sw.Flush();
});
}
此處的「終止狀態」表示任務轉換為 completed, faulted, canceled 狀態。
此指南適用於返回 Task、Task<TResult>、ValueTask<TResult> 或任何類似類型的方法。
規則5:如果構造函數接受Memory <T>作為參數,則假定構造對象上的實例方法是Memory<T>實例的消費者。
請看以下示例:
class OddValueExtractor
{
public OddValueExtractor(ReadOnlyMemory<int> input);
public bool TryReadNextOddValue(out int value);
}
void PrintAllOddValues(ReadOnlyMemory<int> input)
{
var extractor = new OddValueExtractor(input);
while (extractor.TryReadNextOddValue(out int value))
{
Console.WriteLine(value);
}
}
此處的 OddValueExtractor 構造函數接受 ReadOnlyMemory<int> 作為構造函數參數,因此構造函數本身是 ReadOnlyMemory<int> 實例的消費者,並且該實例的所有實例方法也是原始 ReadOnlyMemory<int> 實例的消費者。這意味著 TryReadNextOddValue 消費 ReadOnlyMemory<int> 實例,即使該實例未直接傳遞到 TryReadNextOddValue 方法。
規則 6:如果一個類型具有可寫的 Memory<T> 類型的屬性(或等效的實例方法),則假定該對象上的實例方法是 Memory<T> 實例的消費者。
這是 規則5 的變體。之所以存在此規則,是因為假定使用了可寫屬性或等效方法來捕獲並保留輸入的 Memory<T> 實例,因此同一對象上的實例方法可以利用捕獲的實例。
以下示例觸發了此規則:
class Person
{
// Settable property.
public Memory<char> FirstName { get; set; }
// alternatively, equivalent "setter" method
public SetFirstName(Memory<char> value);
// alternatively, a public settable field
public Memory<char> FirstName;
}
規則 7:如果具有 IMemoryOwner<T> 的引用,則必須在某些時候對其進行處理或轉讓其所有權(但不同時執行兩個操作)。
由於 Memory<T> 實例可能由託管或非託管內存提供支持,因此在對 Memory<T> 實例執行的工作完成之後,所有者必須調用 MemoryPool<T>.Dispose。
此外,所有者可能會將 IMemoryOwner<T> 實例的所有權轉讓給其他組件,同時獲取所有權的組件將負責在適當時間調用 MemoryPool<T>.Dispose
調用 Dispose 方法失敗可能會導致非託管內存洩漏或其他性能降低問題
此規則也適用於調用工廠方法的代碼(如 MemoryPool<T>.Rent)。調用方將成為工廠生產的 IMemoryOwner<T> 的所有者,並負責在完成後 Dispose 該實例。
規則 8:如果 API 接口中具有 IMemoryOwner<T> 參數,即表示你接受該實例的所有權。
接受此類型的實例表示組件打算獲取此實例的所有權。該組件將負責根據 規則7 進行正確處理。
在方法調用完成後,將 IMemoryOwner<T> 實例的所有權轉讓給其他組件,之後該組件將不再使用該實例。
重要:
構造函數接受 IMemoryOwner<T> 作為參數的類應實現接口 IDisposable,並且 Dispose 方法中應調用 MemoryPool<T>.Dispose。
規則 9:如果要封裝同步的 p/invoke 方法,則應接受 Span<T> 作為參數
根據 規則1,Span<T> 通常是用於同步 API 的合規類型。可以通過 fixed 關鍵字固定 Span<T> 實例,如下面的示例所示。
using System.Runtime.InteropServices;
[DllImport(...)]
private static extern unsafe int ExportedMethod(byte* pbData, int cbData);
public unsafe int ManagedWrapper(Span<byte> data)
{
fixed (byte* pbData = &MemoryMarshal.GetReference(data))
{
int retVal = ExportedMethod(pbData, data.Length);
/* error checking retVal goes here */
return retVal;
}
}
在上一示例中,如果輸入 span 為空,則 pbData 可以為 Null。如果 ExportedMethod 方法參數 pbData 不能為 Null,可以按如下示例實現該方法:
public unsafe int ManagedWrapper(Span<byte> data)
{
fixed (byte* pbData = &MemoryMarshal.GetReference(data))
{
byte dummy = 0;
int retVal = ExportedMethod((pbData != null) ? pbData : &dummy, data.Length);
/* error checking retVal goes here */
return retVal;
}
}
規則 10:如果要包裝異步 p/invoke 方法,則應接受 Memory<T> 作為參數
由於 fixed 關鍵字不能在異步操作中使用,因此使用 Memory<T>.Pin 方法固定 Memory<T> 實例,無論實例代表的連續內存是哪種類型。下面的示例演示了如何使用此 API 執行異步 p/invoke 調用。
using System.Runtime.InteropServices;
[UnmanagedFunctionPointer(...)]
private delegate void OnCompletedCallback(IntPtr state, int result);
[DllImport(...)]
private static extern unsafe int ExportedAsyncMethod(byte* pbData, int cbData, IntPtr pState, IntPtr lpfnOnCompletedCallback);
private static readonly IntPtr _callbackPtr = GetCompletionCallbackPointer();
public unsafe Task<int> ManagedWrapperAsync(Memory<byte> data)
{
// setup
var tcs = new TaskCompletionSource<int>();
var state = new MyCompletedCallbackState
{
Tcs = tcs
};
var pState = (IntPtr)GCHandle.Alloc(state);
var memoryHandle = data.Pin();
state.MemoryHandle = memoryHandle;
// make the call
int result;
try
{
result = ExportedAsyncMethod((byte*)memoryHandle.Pointer, data.Length, pState, _callbackPtr);
}
catch
{
((GCHandle)pState).Free(); // cleanup since callback won't be invoked
memoryHandle.Dispose();
throw;
}
if (result != PENDING)
{
// Operation completed synchronously; invoke callback manually
// for result processing and cleanup.
MyCompletedCallbackImplementation(pState, result);
}
return tcs.Task;
}
private static void MyCompletedCallbackImplementation(IntPtr state, int result)
{
GCHandle handle = (GCHandle)state;
var actualState = (MyCompletedCallbackState)(handle.Target);
handle.Free();
actualState.MemoryHandle.Dispose();
/* error checking result goes here */
if (error)
{
actualState.Tcs.SetException(...);
}
else
{
actualState.Tcs.SetResult(result);
}
}
private static IntPtr GetCompletionCallbackPointer()
{
OnCompletedCallback callback = MyCompletedCallbackImplementation;
GCHandle.Alloc(callback); // keep alive for lifetime of application
return Marshal.GetFunctionPointerForDelegate(callback);
}
private class MyCompletedCallbackState
{
public TaskCompletionSource<int> Tcs;
public MemoryHandle MemoryHandle;
}
注:
Memory<T>.Pin 方法返回內存句柄,且垃圾回收器將不會移動此處內存,直到釋放該方法返回的 MemoryHandle 對象為止。這使您可以檢索和使用該內存地址。
版權申明:本文來源於網友收集或網友提供,如果有侵權,請轉告版主或者留言,本公眾號立即刪除。