記憶體包裝類 Memory 和 Span 相關型別
阿新 • • 發佈:2021-02-19
- [1. 前言](#1-前言)
- [2. 簡介](#2-簡介)
- [3. Memory<T>和Span<T>使用準則](#3-memorylttgt和spanlttgt使用準則)
- [3.1. 所有者, 消費者和生命週期管理](#31-所有者-消費者和生命週期管理)
- [3.2. Memory<T> 和所有者/消費者模型](#32-memorylttgt-和所有者消費者模型)
- [3.3. “缺少所有者” 的Memory<T> 例項](#33-缺少所有者-的memorylttgt-例項)
- [3.4. 使用準則](#34-使用準則)
# 1. 前言
此文章是**官方文件**的翻譯,由於官方文件中文版**是機器翻譯**的,有些部分有疏漏和錯誤,所以本人進行了翻譯供大家學習,如有問題歡迎指正。
>**參考資料**:
>[memory-and-spans](https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/) --- Microsoft
# 2. 簡介
.NET 包含多個相互關聯的型別,它們表示任意記憶體的連續的強型別區域。 這些方法包括:
+ `System.Span`
+ 用於訪問連續的記憶體區域
+ 得到該型別的例項:
+ 1個**T**型別的**陣列**
+ 1個`String`
+ 1個使用 `stackalloc` 分配的緩衝區
+ 1個指向非託管記憶體的**指標**
+ 例項必須儲存在**堆疊**(stack)上,因此有很對限制
+ **類的欄位**不能是此型別
+ 不能在**非同步**操作中使用
+ `System.ReadOnlySpan`
+ `Span` 結構體的不可變版本
+ `System.Memory`
+ 連續的記憶體區域的包裝器
+ 例項建立
+ `T` 型別陣列
+ `String`
+ 記憶體管理器
+ 例項可以儲存在**託管堆**(managed heap)上,所以它沒有 `Span` 的限制
+ `System.ReadOnlyMemory`
+ `Memory` 結構的不可變版本。
+ `System.Buffers.MemoryPool`
+ 它將強型別記憶體塊從記憶體池分配給所有者
+ `IMemoryOwner` 例項可以通過呼叫 `MemoryPool.Rent` 從池中租用
+ 通過呼叫 `MemoryPool.Dispose()` 將其釋放回池中
+ `System.Buffers.IMemoryOwner`
+ 表示記憶體塊的所有者,管理其生命週期
+ `MemoryManager`
+ 一個抽象基類,可用於替換 `Memory` 的實現,以便 `Memory` 可以由其他型別(如安全控制代碼(safe handles))提供支援
+ MemoryManager<T> 適用於高階方案。
+ `ArraySegment`
+ 是陣列的包裝,對應陣列中,從特定索引開始的特定數量的一系列元素
+ `System.MemoryExtensions`
+ 用於將String、陣列和陣列段(`ArraySegment`)轉換為 `Memory` 塊的擴充套件方法集
`System.Span`、`System.Memory` 及其對應的只讀型別被設計為:
+ **避免**不必要地**複製記憶體**或在託管堆上進行**記憶體分配**
+ 通過 `Slice` 方法或這些型別的的建構函式建立它們, 並不涉及複製**底層緩衝**(underlying buffers): 只更新相關引用和偏移
+ 形象的說就是,只更新我們可以訪問到的記憶體的位置和範圍,而不是將這些記憶體資料複製出來
> 備註:
> 對於早期框架,`Span` 和 `Memory` 在 [System.Memory NuGet](https://www.nuget.org/packages/System.Memory/) 包中提供。
使用 memory 和 span
+ 由於 memory 和 span 相關型別通常用於在處理 pipeline 中儲存資料,因此開發人員在使用 `Span`、`Memory` 和相關型別時要務必遵循一套最佳做法。 `Memory` 和`Span` 使用準則中介紹了這些最佳做法。
# 3. Memory<T>和Span<T>使用準則
+ `Span` 和 `ReadOnlySpan`
+ 是可由託管或非託管記憶體提供支援的輕量級記憶體緩衝區
+ `Memory` 及其相關型別
+ 由託管和非託管記憶體提供支援
+ 與 `Span` 不同,`Memory` 可以儲存在託管堆上
`Span` 和 `Memory` 都是可用於 pipeline 的結構化資料的緩衝區。
+ 它們設計的目的是將某些或所有資料有效地傳遞到 pipeline 中的元件,這些元件可以對其進行處理並修改(可選)緩衝區
+ 由於 `Memory` 及其相關型別可由多個元件或多個執行緒訪問,因此開發人員必須遵循一些標準使用準則才能生成可靠的程式碼
+
## 3.1. 所有者, 消費者和生命週期管理
由於可以在各個 API 之間傳送緩衝區,以及由於**緩衝區**有時可以從多個執行緒進行訪問,因此請務必考慮生命週期管理。 下面介紹三個核心概念:
+ 所有權:
+ 緩衝區例項的**所有者**負責生命週期管理,包括當不再使用緩衝區時將其銷燬
+ 所有緩衝區都擁有一個所有者
+ 通常,所有者是建立緩衝區或從工廠接收緩衝區的元件
+ 所有權也可以轉讓;
+ 元件 A 可以將緩衝區的控制權轉讓給元件 B,此時元件 A 就無法再使用該緩衝區,元件 B 將負責在不再使用緩衝區時將其銷燬。
+ 消費:
+ 允許緩衝區例項的**消費者**通過讀取和寫入來使用**緩衝區例項**
+ 緩衝區一次可以擁有一個**消費者**,除非提供了某些外部同步機制
+ 緩衝區的**活躍消費者**不一定是緩衝區的**所有者**
+ 租約:
+ 租約是指允許特定元件在一個時間長度範圍內成為緩衝區消費者
以下虛擬碼示例闡釋了這三個概念。 它包括:
+ 例項化型別為 `Char` 的 `Memory` 緩衝區的
+ 呼叫 `WriteInt32ToBuffer` 方法以將整數的字串表示形式寫入緩衝區
+ 然後呼叫 `DisplayBufferToConsole` 方法以顯示緩衝區的值。
```csharp
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();
}
}
}
```
+ 所有者
+ Main 方法建立緩衝區(在此示例中為 `Span` 例項),因此它是其所有者。 因此,Main 將負責在不再使用緩衝區時將其銷燬。
+ 消費者
+ `WriteInt32ToBuffer` 和 `DisplayBufferToConsole`
+ 一次只能有一個消費者
+ 先是 `WriteInt32ToBuffer` ,然後是 `DisplayBufferToConsole`
+ 這兩個消費者都不擁有緩衝區
+ 此上下文中的“消費者”並不意味著以只讀形式檢視緩衝區;如果提供了以讀/寫形式檢視緩衝區的許可權,則消費者可以像 `WriteInt32ToBuffer` 那樣修改緩衝區的內容
+ 租約
+ `WriteInt32ToBuffer` 方法在方法呼叫的開始時間和方法返回的時間之間會**租用**(能消費的)緩衝區
+ `DisplayBufferToConsole` 在執行時會租用緩衝區,方法返回時將解除租用
+ 沒有用於租約管理的 API,“租用”是概念性內容
## 3.2. Memory<T> 和所有者/消費者模型
.NET Core 支援以下兩種所有權模型:
+ 支援單個所有權的模型
+ 緩衝區在其整個生存期內擁有單個所有者。
+ 支援所有權轉讓的模型
+ 緩衝區的所有權可以從其原始所有者(其建立者)轉讓給其他元件,該元件隨後將負責緩衝區的生存期管理
+ 該所有者可以反過來將所有權轉讓給其他元件等
使用 `System.Buffers.IMemoryOwner` 介面**顯式的管理**緩衝區的所有權。
+ `IMemoryOwner` 支援上述這兩種所有權模型
+ 具有 `IMemoryOwner` 引用的元件擁有緩衝區
+ 以下示例使用 `IMemoryOwner` 例項反映 `Memory` 緩衝區的所有權。
```csharp
using System;
using System.Buffers;
class Example
{
static void Main()
{
IMemoryOwner owner = MemoryPool.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 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 buffer) =>
Console.WriteLine($"Contents of the buffer: '{buffer}'");
}
```
也可以使用 **using** 編寫此示例:
```csharp
using System;
using System.Buffers;
class Example
{
static void Main()
{
using (IMemoryOwner owner = MemoryPool.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 buffer)
{
var strValue = 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` 例項的引用,因此 **Main** 方法是緩衝區的**所有者**。
+ `WriteInt32ToBuffer` 和 `DisplayBufferToConsole` 方法接受 ``Memory` `引數作為公共 API。 因此,它們是緩衝區的消費者。 並且它們同一時間僅有一個消費者
儘管 `WriteInt32ToBuffer` 方法用於將資料寫入緩衝區,但 `DisplayBufferToConsole` 方法並不如此。
+ 若要反映此情況,方法引數型別可改為 `ReadOnlyMemory`
## 3.3. “缺少所有者” 的Memory<T> 例項
無需使用 `IMemoryOwner` 即可建立 `Memory` 例項。 在這種情況下,緩衝區的所有權是**隱式的**,並且僅支援**單所有者模型**。 可以通過以下方式達到此目的:
+ 直接呼叫 `Memory` 建構函式之一,傳入 `T[]`,如下面的示例所示
+ 呼叫 `String.AsMemory` 擴充套件方法以生成 `ReadOnlyMemory` 例項
```csharp
using System;
class Example
{
static void Main()
{
Memory 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 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` 例項的方法是緩衝區的**隱式所有者**。 無法將**所有權**轉讓給任何其他元件, 因為沒有 `IMemoryOwner` 例項可用於進行轉讓
+ 也可以**假設**執行時的**垃圾回收器擁有緩衝區**,全部的方法只消費緩衝區
## 3.4. 使用準則
因為擁有一個記憶體塊,但打算將其傳遞給多個元件,其中一些元件可能同時在特定的記憶體塊上執行,所以建立使用`Memory`和`Span`的準則是很必要的,因為:
+ 所有者釋放它之後,一個元件還可能會保留對該儲存塊的引用。
+ 兩個元件可能併發的同時在緩衝區上進行操作,從而破壞了緩衝區中的資料。
+ 儘管`Span`的**堆疊分配性質**優化了效能,而且使`Span`成為在記憶體塊上執行的**首選型別**,但它也使`Span`受到一些主要限制
+ **重要**的是要知道**何時**使用`Span`以及**何時**使用 `Memory`
下面介紹成功使用 `Memory` 及其相關型別的建議。 除非另有明確說明,否則適用於 `Memory` 和 `Span` 的指南也適用於 `ReadOnlyMemory` 和 `ReadOnlySpan` 。
**規則 1:對於同步 API,如有可能,請使用 Span<T>(而不是 Memory<T>)作為引數。**
`Span` 比 `Memory` 更多功能:
+ 可以表示更多種類的連續記憶體緩衝區
+ `Span` 還提供比 `Memory` 更好的效能
+ 無法進行 `Span` 到 `Memory` 的轉換
+ 可以使用 `Memory.Span` 屬性將 `Memory` 例項轉換為 `Span`
+ 如果呼叫方恰好具有 `Memory` 例項,則它們不管怎樣都可以使用 `Span` 引數呼叫你的方法
使用型別 `Span`(而不是型別 `Memory`)作為方法的引數型別還可以幫助你編寫正確的消費方法實現。 你將自動進行編譯時檢查,以確保不會企圖訪問此方法租約之外的緩衝區
有時,必須使用 `Memory` 引數(而不是 `Span` 引數),即使完全同步也是如此。 所依賴的 API 可能僅接受 `Memory` 引數。 這沒有問題,但當使用同步的 `Memory` 時,應注意權衡利弊
**規則 2:如果緩衝區應為只讀,則使用 ReadOnlySpan<T> 或 ReadOnlyMemory<T>**
在前面的示例中,`DisplayBufferToConsole` 方法僅從緩衝區讀取資料;它不修改緩衝區的內容。 方法簽名應進行修改如下。
```csharp
void DisplayBufferToConsole(ReadOnlyMemory buffer);
```
事實上,如果我們結合 規則1 和 規則2 ,我們可以做得更好,並重寫方法簽名如下:
```csharp
void DisplayBufferToConsole(ReadOnlySpan buffer);
```
`DisplayBufferToConsole` 方法現在幾乎適用於每一個能夠想到的緩衝區型別:
+ `T[]`、使用 `stackalloc` 分配的儲存 等等
+ 甚至可以向其直接傳遞 `String`!
**規則 3:如果方法接受 Memory<T> 並返回 void,則在方法返回之後不得使用 Memory<T> 例項。**
這與前面提到的“租約”概念相關。 返回 void 的方法對 `Memory` 例項的租用將在進入該方法時開始,並在退出該方法時結束。 請考慮以下示例,該示例會基於控制檯中的輸入在迴圈中呼叫 Log。
```csharp
using System;
using System.Buffers;
public class Example
{
// implementation provided by third party
static extern void Log(ReadOnlyMemory message);
// user code
public static void Main()
{
using (var owner = MemoryPool.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 span)
{
string strValue = value.ToString();
int length = strValue.Length;
strValue.AsSpan().CopyTo(span.Slice(0, length));
return length;
}
}
```
如果 `Log` 是完全同步的方法,則此程式碼將按預期執行,因為在任何給定時間只有一個活躍的記憶體例項消費者。 但是,請想象Log具有此實現。
```csharp
// !!! INCORRECT IMPLEMENTATION !!!
static void Log(ReadOnlyMemory 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` 例項。 Main 方法可能會在 Log 嘗試從緩衝區進行讀取時更改緩衝區資料,這可能導致消費者在使用快取區資料時資料已經被修改。
有多種方法可解決此問題:
+ Log 方法可以按以下所示,返回 Task,而不是 void。
```csharp
// An acceptable implementation.
static Task Log(ReadOnlyMemory 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:
```csharp
// An acceptable implementation.
static void Log(ReadOnlyMemory 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` 方法:
```csharp
// An acceptable implementation.
static Task Log(ReadOnlyMemory 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`、`ValueTask` 或任何類似型別的方法。
**規則5:如果建構函式接受Memory <T>作為引數,則假定構造物件上的例項方法是Memory<T>例項的消費者。**
請看以下示例:
```csharp
class OddValueExtractor
{
public OddValueExtractor(ReadOnlyMemory input);
public bool TryReadNextOddValue(out int value);
}
void PrintAllOddValues(ReadOnlyMemory input)
{
var extractor = new OddValueExtractor(input);
while (extractor.TryReadNextOddValue(out int value))
{
Console.WriteLine(value);
}
}
```
此處的 `OddValueExtractor` 建構函式接受 `ReadOnlyMemory` 作為建構函式引數,因此建構函式本身是 `ReadOnlyMemory` 例項的消費者,並且該例項的所有例項方法也是原始 `ReadOnlyMemory` 例項的消費者。 這意味著 `TryReadNextOddValue` 消費 `ReadOnlyMemory` 例項,即使該例項未直接傳遞到 `TryReadNextOddValue` 方法。
**規則 6:如果一個型別具有可寫的 Memory<T> 型別的屬性(或等效的例項方法),則假定該物件上的例項方法是 Memory<T> 例項的消費者。**
這是 規則5 的變體。之所以存在此規則,是因為假定使用了可寫屬性或等效方法來捕獲並保留輸入的 `Memory` 例項,因此同一物件上的例項方法可以利用捕獲的例項。
以下示例觸發了此規則:
```csharp
class Person
{
// Settable property.
public Memory FirstName { get; set; }
// alternatively, equivalent "setter" method
public SetFirstName(Memory value);
// alternatively, a public settable field
public Memory FirstName;
}
```
**規則 7:如果具有 `IMemoryOwner` 的引用,則必須在某些時候對其進行處理或轉讓其所有權(但不同時執行兩個操作)。**
+ 由於 `Memory` 例項可能由託管或非託管記憶體提供支援,因此在對 `Memory` 例項執行的工作完成之後,所有者必須呼叫 `MemoryPool.Dispose`。
+ 此外,所有者可能會將 `IMemoryOwner` 例項的所有權轉讓給其他元件,同時獲取所有權的元件將負責在適當時間呼叫 `MemoryPool.Dispose`
+ 呼叫 Dispose 方法失敗可能會導致非託管記憶體洩漏或其他效能降低問題
+ 此規則也適用於呼叫工廠方法的程式碼(如 `MemoryPool.Rent`)。 呼叫方將成為工廠生產的 `IMemoryOwner` 的所有者,並負責在完成後 Dispose 該例項。
**規則 8:如果 API 介面中具有 `IMemoryOwner` 引數,即表示你接受該例項的所有權。**
接受此型別的例項表示元件打算獲取此例項的所有權。 該元件將負責根據 **規則7** 進行正確處理。
在方法呼叫完成後,將 `IMemoryOwner` 例項的所有權轉讓給其他元件,之後該元件將不再使用該例項。
>**重要:**
>建構函式接受 `IMemoryOwner` 作為引數的類應實現介面 `IDisposable`,並且 `Dispose` 方法中應呼叫 `MemoryPool.Dispose`。
**規則 9:如果要封裝同步的 p/invoke 方法,則應接受 Span<T> 作為引數**
根據 規則1,`Span` 通常是用於同步 API 的合規型別。 可以通過 `fixed` 關鍵字固定 `Span` 例項,如下面的示例所示。
```csharp
using System.Runtime.InteropServices;
[DllImport(...)]
private static extern unsafe int ExportedMethod(byte* pbData, int cbData);
public unsafe int ManagedWrapper(Span 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,可以按如下示例實現該方法:
```cs
public unsafe int ManagedWrapper(Span 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.Pin` 方法固定 `Memory` 例項,無論例項代表的連續記憶體是哪種型別。 下面的示例演示瞭如何使用此 API 執行非同步 p/invoke 呼叫。
```csharp
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 ManagedWrapperAsync(Memory data)
{
// setup
var tcs = new TaskCompletionSource();
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 Tcs;
public MemoryHandle MemoryHandle;
}
```
>注:
>`Memory.Pin` 方法返回記憶體控制代碼,且垃圾回收器將不會移動此處記憶體,直到釋放該方法返回的 `MemoryHandle` 物件為止。這使您可以檢索和使用該記憶體