- 1. 前言
- 2. 簡介
- 3. Memory<T>和Span<T>使用準則
- 3.1. 所有者, 消費者和生命周期管理
- 3.2. Memory<T> 和所有者/消費者模型
- 3.3. “缺少所有者” 的Memory<T> 實體
- 3.4. 使用準則
1. 前言
此文章是官方檔案的翻譯,由于官方檔案中文版是機器翻譯的,有些部分有疏漏和錯誤,所以本人進行了翻譯供大家學習,如有問題歡迎指正,
參考資料:
memory-and-spans --- Microsoft
2. 簡介
.NET 包含多個相互關聯的型別,它們表示任意記憶體的連續的強型別區域, 這些方法包括:
-
System.Span<T>- 用于訪問連續的記憶體區域
- 得到該型別的實體:
- 1個T型別的陣列
- 1個
String - 1個使用
stackalloc分配的緩沖區 - 1個指向非托管記憶體的指標
- 實體必須存盤在堆疊(stack)上,因此有很對限制
- 類的欄位不能是此型別
- 不能在異步操作中使用
-
System.ReadOnlySpan<T>Span<T>結構體的不可變版本
-
System.Memory<T>- 連續的記憶體區域的包裝器
- 實體創建
T型別陣列String- 記憶體管理器
- 實體可以存盤在托管堆(managed heap)上,所以它沒有
Span<T>的限制
-
System.ReadOnlyMemory<T>Memory<T>結構的不可變版本,
-
System.Buffers.MemoryPool<T>- 它將強型別記憶體塊從記憶體池分配給所有者
IMemoryOwner<T>實體可以通過呼叫MemoryPool<T>.Rent從池中租用- 通過呼叫
MemoryPool<T>.Dispose()將其釋放回池中
- 它將強型別記憶體塊從記憶體池分配給所有者
-
System.Buffers.IMemoryOwner<T>- 表示記憶體塊的所有者,管理其生命周期
-
MemoryManager<T>- 一個抽象基類,可用于替換
Memory<T>的實作,以便Memory<T>可以由其他型別(如安全句柄(safe handles))提供支持 - MemoryManager<T> 適用于高級方案,
- 一個抽象基類,可用于替換
-
ArraySegment<T>- 是陣列的包裝,對應陣列中,從特定索引開始的特定數量的一系列元素
-
System.MemoryExtensions- 用于將String、陣列和陣列段(
ArraySegment<T>)轉換為Memory<T>塊的擴展方法集
- 用于將String、陣列和陣列段(
System.Span<T>、System.Memory<T> 及其對應的只讀型別被設計為:
- 避免不必要地復制記憶體或在托管堆上進行記憶體分配
- 通過
Slice方法或這些型別的的建構式創建它們, 并不涉及復制底層緩沖(underlying buffers): 只更新相關參考和偏移- 形象的說就是,只更新我們可以訪問到的記憶體的位置和范圍,而不是將這些記憶體資料復制出來
備注:
對于早期框架,Span<T>和Memory<T>在 System.Memory NuGet 包中提供,
使用 memory 和 span
- 由于 memory 和 span 相關型別通常用于在處理 pipeline 中存盤資料,因此開發人員在使用
Span<T>、Memory<T>和相關型別時要務必遵循一套最佳做法,Memory<T>和Span<T>使用準則中介紹了這些最佳做法,
3. Memory<T>和Span<T>使用準則
Span<T>和ReadOnlySpan<T>- 是可由托管或非托管記憶體提供支持的輕量級記憶體緩沖區
Memory<T>及其相關型別- 由托管和非托管記憶體提供支持
- 與
Span<T>不同,Memory<T>可以存盤在托管堆上
Span<T> 和 Memory<T> 都是可用于 pipeline 的結構化資料的緩沖區,
- 它們設計的目的是將某些或所有資料有效地傳遞到 pipeline 中的組件,這些組件可以對其進行處理并修改(可選)緩沖區
- 由于
Memory<T>及其相關型別可由多個組件或多個執行緒訪問,因此開發人員必須遵循一些標準使用準則才能生成可靠的代碼
3.1. 所有者, 消費者和生命周期管理
由于可以在各個 API 之間傳送緩沖區,以及由于緩沖區有時可以從多個執行緒進行訪問,因此請務必考慮生命周期管理, 下面介紹三個核心概念:
- 所有權:
- 緩沖區實體的所有者負責生命周期管理,包括當不再使用緩沖區時將其銷毀
- 所有緩沖區都擁有一個所有者
- 通常,所有者是創建緩沖區或從工廠接識訓沖區的組件
- 所有權也可以轉讓;
- 組件 A 可以將緩沖區的控制權轉讓給組件 B,此時組件 A 就無法再使用該緩沖區,組件 B 將負責在不再使用緩沖區時將其銷毀,
- 消費:
- 允許緩沖區實體的消費者通過讀取和寫入來使用緩沖區實體
- 緩沖區一次可以擁有一個消費者,除非提供了某些外部同步機制
- 緩沖區的活躍消費者不一定是緩沖區的所有者
- 租約:
- 租約是指允許特定組件在一個時間長度范圍內成為緩沖區消費者
以下偽代碼示例闡釋了這三個概念, 它包括:
- 實體化型別為
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 = https://www.cnblogs.com/BigBrotherStone/p/Int32.Parse(Console.ReadLine());
WriteInt32ToBuffer(value, buffer);
DisplayBufferToConsole(buffer);
}
finally
{
buffer.Destroy();
}
}
}
- 所有者
- Main 方法創建緩沖區(在此示例中為
Span<T>實體),因此它是其所有者, 因此,Main 將負責在不再使用緩沖區時將其銷毀,
- Main 方法創建緩沖區(在此示例中為
- 消費者
WriteInt32ToBuffer和DisplayBufferToConsole- 一次只能有一個消費者
- 先是
WriteInt32ToBuffer,然后是DisplayBufferToConsole
- 先是
- 這兩個消費者都不擁有緩沖區
- 此背景關系中的“消費者”并不意味著以只讀形式查看緩沖區;如果提供了以讀/寫形式查看緩沖區的權限,則消費者可以像
WriteInt32ToBuffer那樣修改緩沖區的內容
- 租約
WriteInt32ToBuffer方法在方法呼叫的開始時間和方法回傳的時間之間會租用(能消費的)緩沖區DisplayBufferToConsole在執行時會租用緩沖區,方法回傳時將解除租用- 沒有用于租約管理的 API,“租用”是概念性內容
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 = https://www.cnblogs.com/BigBrotherStone/p/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 = https://www.cnblogs.com/BigBrotherStone/p/value.ToString();
var span = buffer.Span;
for (int ctr = 0; ctr < strValue.Length; ctr++)
span[ctr] = strValue[ctr];
}
static void DisplayBufferToConsole(Memory 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 = https://www.cnblogs.com/BigBrotherStone/p/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 = https://www.cnblogs.com/BigBrotherStone/p/value.ToString();
var span = buffer.Slice(0, strValue.Length).Span;
strValue.AsSpan().CopyTo(span);
}
static void DisplayBufferToConsole(Memory buffer) =>
Console.WriteLine($"Contents of the buffer: '{buffer}'");
}
在此代碼中:
- Main 方法保持對
IMemoryOwner<T>實體的參考,因此 Main 方法是緩沖區的所有者, WriteInt32ToBuffer和DisplayBufferToConsole方法接受Memory<T>引數作為公共 API, 因此,它們是緩沖區的消費者, 并且它們同一時間僅有一個消費者
盡管 WriteInt32ToBuffer 方法用于將資料寫入緩沖區,但 DisplayBufferToConsole 方法并不如此,
- 若要反映此情況,方法引數型別可改為
ReadOnlyMemory<T>
3.3. “缺少所有者” 的Memory<T> 實體
無需使用 IMemoryOwner<T> 即可創建 Memory<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 = https://www.cnblogs.com/BigBrotherStone/p/Int32.Parse(Console.ReadLine());
WriteInt32ToBuffer(value, memory);
DisplayBufferToConsole(memory);
}
static void WriteInt32ToBuffer(int value, Memory buffer)
{
var strValue = value.ToString();
strValue.AsSpan().CopyTo(buffer.Slice(0, strValue.Length).Span);
}
static void DisplayBufferToConsole(Memory buffer) =>
Console.WriteLine($"Contents of the buffer: '{buffer}'");
}
- 最初創建
Memory<T>實體的方法是緩沖區的隱式所有者, 無法將所有權轉讓給任何其他組件, 因為沒有IMemoryOwner<T>實體可用于進行轉讓- 也可以假設運行時的垃圾回收器擁有緩沖區,全部的方法只消費緩沖區
3.4. 使用準則
因為擁有一個記憶體塊,但打算將其傳遞給多個組件,其中一些組件可能同時在特定的記憶體塊上運行,所以建立使用Memory<T>和Span<T>的準則是很必要的,因為:
- 所有者釋放它之后,一個組件還可能會保留對該存盤塊的參考,
- 兩個組件可能并發的同時在緩沖區上進行操作,從而破壞了緩沖區中的資料,
- 盡管
Span<T>的堆疊分配性質優化了性能,而且使Span<T>成為在記憶體塊上運行的首選型別,但它也使Span<T>受到一些主要限制- 重要的是要知道何時使用
Span<T>以及何時使用Memory<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>- 如果呼叫方恰好具有
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 方法現在幾乎適用于每一個能夠想到的緩沖區型別:
T[]、使用stackalloc分配的存盤 等等- 甚至可以向其直接傳遞
String!
規則 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 = https://www.cnblogs.com/BigBrotherStone/p/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 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 = https://www.cnblogs.com/BigBrotherStone/p/&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 = https://www.cnblogs.com/BigBrotherStone/p/&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物件為止,這使您可以檢索和使用該記憶體地址,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/261213.html
標籤:.NET Core
