Panduan penggunaan Memori<T> dan Rentang<T>

.NET menyertakan sejumlah jenis yang mewakili wilayah memori yang berdekatan secara arbitrer. Span<T> dan ReadOnlySpan<T> adalah buffer memori ringan yang membungkus referensi ke memori terkelola atau tidak terkelola. Karena jenis ini hanya dapat disimpan di tumpukan, jenis tersebut tidak cocok untuk skenario seperti panggilan metode asinkron. Untuk mengatasi masalah ini, .NET 2.1 menambahkan beberapa jenis tambahan, termasuk Memory<T>, , ReadOnlyMemory<T>IMemoryOwner<T>, dan MemoryPool<T>. Seperti Span<T>, Memory<T> dan jenis terkaitnya dapat didukung oleh memori terkelola dan tidak dikelola. Tidak seperti Span<T>, Memory<T> dapat disimpan di heap memori terkelola.

Keduanya Span<T> dan Memory<T> merupakan pembungkus atas buffer data terstruktur yang dapat digunakan dalam alur. Artinya, data tersebut dirancang sehingga beberapa atau semua data dapat diteruskan secara efisien ke komponen dalam alur kerja, yang dapat memprosesnya dan jika diperlukan, memodifikasi buffer. Karena Memory<T> dan jenis terkaitnya dapat diakses oleh beberapa komponen atau oleh beberapa utas, penting untuk mengikuti pedoman penggunaan standar agar dapat menghasilkan kode yang andal.

Pemilik, konsumen, dan manajemen seumur hidup

Buffer dapat diteruskan antara API dan terkadang dapat diakses dari beberapa utas, jadi ketahui bagaimana masa pakai buffer dikelola. Ada tiga konsep inti:

  • Kepemilikan. Pemilik instans buffer bertanggung jawab atas manajemen seumur hidup, termasuk menghancurkan buffer saat tidak lagi digunakan. Semua buffer memiliki satu pemilik. Umumnya pemilik adalah komponen yang membuat buffer atau yang menerima buffer dari pabrik. Kepemilikan juga dapat ditransfer; Component-A dapat melepaskan kontrol buffer ke Component-B, di mana Component-A mungkin tidak lagi menggunakan buffer, dan Component-B menjadi bertanggung jawab untuk menghancurkan buffer ketika tidak lagi digunakan.

  • Konsumsi. Konsumen instans buffer diizinkan untuk menggunakan instans buffer dengan membaca darinya dan mungkin menulis ke dalamnya. Penampung hanya dapat memiliki satu konsumen dalam satu waktu, kecuali jika disediakan mekanisme sinkronisasi eksternal. Konsumen aktif dari sebuah buffer belum tentu menjadi pemiliknya.

  • Sewa. Leasing adalah lamanya waktu komponen tertentu diizinkan untuk memanfaatkan buffer.

Contoh kode pseudo berikut mengilustrasikan ketiga konsep ini. Buffer dalam kode pseudo menunjukkan buffer Memory<T> atau Span<T> dari jenis Char. Metode Main menginstansiasi buffer, memanggil metode WriteInt32ToBuffer untuk menulis representasi string dari bilangan bulat ke buffer, dan kemudian memanggil metode DisplayBufferToConsole untuk menampilkan nilai buffer.

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

Metode Main ini membuat buffer dan menjadi pemiliknya. Oleh karena itu, Main bertanggung jawab untuk menghancurkan buffer ketika tidak lagi digunakan. Kode pseudocode mengilustrasikan ini dengan memanggil metode Destroy pada buffer. (Baik Memory<T> maupun Span<T> sebenarnya tidak memiliki Destroy metode. Anda akan melihat contoh kode aktual nanti di artikel ini.)

Buffer memiliki dua pengguna, WriteInt32ToBuffer dan DisplayBufferToConsole. Hanya ada satu konsumen pada satu waktu (pertama WriteInt32ToBuffer, kemudian DisplayBufferToConsole), dan tidak satu pun dari konsumen yang memiliki buffer. Perhatikan juga bahwa "konsumen" dalam konteks ini tidak menyiratkan tampilan buffer yang hanya untuk membaca; konsumen dapat memodifikasi konten buffer, seperti yang dilakukan WriteInt32ToBuffer, jika diberi tampilan buffer yang memungkinkan baca dan tulis.

Metode WriteInt32ToBuffer ini dapat mengonsumsi buffer antara awal pemanggilan metode dan saat metode selesai. Demikian pula, DisplayBufferToConsole memiliki hak akses pada buffer saat dieksekusi, dan hak akses dilepaskan ketika metode selesai dieksekusi. (Tidak ada API untuk manajemen sewa; "sewa" adalah masalah konseptual.)

Memori<T> dan model pemilik/pengguna

Sebagaimana dicatat dalam bagian Pemilik, konsumen, dan manajemen seumur hidup, buffer selalu memiliki pemilik. .NET mendukung dua model kepemilikan:

  • Model yang mendukung kepemilikan tunggal. Buffer memiliki satu pemilik selama masa hidupnya.
  • Model yang mendukung transfer kepemilikan. Kepemilikan buffer dapat ditransfer dari pemilik aslinya (pembuatnya) ke komponen lain, yang kemudian bertanggung jawab atas manajemen seumur hidup buffer. Pemilik tersebut pada gilirannya dapat mentransfer kepemilikan ke komponen lain, dan sebagainya.

Anda menggunakan System.Buffers.IMemoryOwner<T> antarmuka untuk mengelola kepemilikan buffer secara eksplisit. IMemoryOwner<T> mendukung kedua model kepemilikan. Komponen yang memiliki referensi IMemoryOwner<T> adalah pemilik buffer. Contoh berikut menggunakan instans IMemoryOwner<T> untuk mencerminkan kepemilikan Memory<T> buffer.

using System;
using System.Buffers;

class Example
{
    static void Main()
    {
        IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent();

        Console.Write("Enter a number: ");
        try
        {
            string? s = Console.ReadLine();

            if (s is null)
                return;

            var value = Int32.Parse(s);

            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}'");
}

Anda juga dapat menulis contoh ini dengan using pernyataan:

using System;
using System.Buffers;

class Example
{
    static void Main()
    {
        using (IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent())
        {
            Console.Write("Enter a number: ");
            try
            {
                string? s = Console.ReadLine();

                if (s is null)
                    return;

                var value = Int32.Parse(s);

                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}'");
}

Dalam kode ini:

  • Metode Main menyimpan referensi ke instance IMemoryOwner<T>, sehingga metode Main menjadi pemilik dari buffer tersebut.
  • Metode WriteInt32ToBuffer dan DisplayBufferToConsole menerima Memory<T> sebagai API publik. Oleh karena itu, mereka adalah konsumen penyimpan cadangan. Metode ini mengonsumsi buffer satu per satu.

Meskipun WriteInt32ToBuffer metode dimaksudkan untuk menulis nilai ke buffer, DisplayBufferToConsole metode tidak dimaksudkan untuk melakukannya. Untuk mencerminkan hal ini, bisa saja menerima argumen jenis ReadOnlyMemory<T>. Untuk informasi selengkapnya tentang ReadOnlyMemory<T>, lihat Aturan #2: Gunakan ReadOnlySpan<T> atau ReadOnlyMemory<T> jika buffer harus bersifat baca-saja.

"Tanpa Pemilik" Memori< Instans T>

Anda dapat membuat Memory<T> instans tanpa menggunakan IMemoryOwner<T>. Dalam hal ini, kepemilikan buffer bersifat implisit daripada eksplisit, dan hanya model pemilik tunggal yang didukung. Anda dapat melakukan ini dengan:

  • Memanggil salah satu Memory<T> konstruktor secara langsung, meneruskan objek T[], seperti contoh berikut.
  • Memanggil metode ekstensi String.AsMemory untuk menghasilkan ReadOnlyMemory<char> instans.
using System;

class Example
{
    static void Main()
    {
        Memory<char> memory = new char[64];

        Console.Write("Enter a number: ");
        string? s = Console.ReadLine();

        if (s is null)
            return;

        var value = Int32.Parse(s);

        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}'");
}

Metode yang awalnya membuat instance Memory<T> adalah pemilik implisit buffer. Kepemilikan tidak dapat ditransfer ke komponen lain karena tidak IMemoryOwner<T> ada instans untuk memfasilitasi transfer. (Sebagai alternatif, Anda juga dapat membayangkan bahwa pengumpul sampah dari sistem runtime memiliki buffer, dan semua metode hanya mengonsumsi buffer.)

Panduan penggunaan

Karena blok memori dimiliki namun dimaksudkan untuk diberikan kepada beberapa komponen, beberapa di antaranya mungkin beroperasi pada blok memori tertentu secara bersamaan, penting untuk menetapkan panduan untuk menggunakan baik Memory<T> maupun Span<T>. Pedoman diperlukan karena dimungkinkan bagi komponen untuk:

  • Pertahankan referensi ke blok memori setelah pemiliknya merilisnya.
  • Beroperasi pada buffer pada saat yang sama ketika komponen lain beroperasi di atasnya, dalam proses merusak data di buffer.

Meskipun sifat alokasi tumpukan Span<T> mengoptimalkan performa dan menjadikan Span<T> sebagai tipe yang lebih disukai untuk beroperasi pada blok memori, Span<T> juga dihadapkan pada beberapa pembatasan utama. Penting untuk mengetahui kapan harus menggunakan Span<T> dan kapan harus menggunakan Memory<T>.

Berikut ini adalah rekomendasi kami untuk berhasil menggunakan Memory<T> dan jenis terkaitnya. Panduan yang berlaku untuk Memory<T> dan Span<T> juga berlaku untuk ReadOnlyMemory<T> dan ReadOnlySpan<T> kecuali disebutkan sebaliknya.

Aturan #1: Untuk API sinkron, gunakan Span<T> alih-alih Memori<T> sebagai parameter jika memungkinkan

Span<T> lebih serbaguna daripada Memory<T> dan dapat mewakili berbagai buffer memori yang berdampingan yang lebih luas. Span<T> juga menawarkan performa yang lebih baik daripada Memory<T>. Terakhir, Anda dapat menggunakan properti Memory<T>.Span untuk mengonversi instance Memory<T> menjadi Span<T>, meskipun konversi Span<T>-to-Memory<T> tidak dimungkinkan. Jadi, jika penelepon Anda kebetulan memegang Memory<T> instans, mereka tetap dapat memanggil metode Anda dengan Span<T> parameter.

Menggunakan parameter jenis Span<T> alih-alih jenis Memory<T> juga membantu Anda menulis implementasi metode konsumsi yang benar. Anda akan secara otomatis memperoleh pemeriksaan pada waktu kompilasi untuk memastikan bahwa Anda tidak mencoba mengakses buffer melewati batas metode Anda (selengkapnya tentang ini nanti).

Terkadang, Anda harus menggunakan Memory<T> parameter alih-alih Span<T> parameter, bahkan jika Anda sepenuhnya sinkron. Mungkin API yang Anda andalkan hanya menerima argumen Memory<T>. Ini baik-baik saja, tetapi waspadai tradeoff yang terlibat saat menggunakan Memory<T> secara sinkron.

Aturan #2: Gunakan ReadOnlySpan<T> atau ReadOnlyMemory<T> jika buffer harus baca-saja

Dalam contoh sebelumnya, DisplayBufferToConsole metode hanya membaca dari buffer; tidak mengubah konten buffer. Penandatanganan metode harus diubah menjadi yang berikut ini.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

Bahkan, jika Anda menggabungkan aturan ini dan Aturan #1, kita dapat melakukan lebih baik dan menulis ulang tanda tangan metode sebagai berikut:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

Metode DisplayBufferToConsole ini sekarang berfungsi dengan hampir setiap jenis buffer yang dapat dibayangkan: T[], penyimpanan yang dialokasikan dengan stackalloc, dan sebagainya. Anda bahkan dapat meneruskan String langsung ke dalamnya! Untuk informasi selengkapnya, lihat Masalah GitHub dotnet/docs #25551.

Aturan #3: Jika metode Anda menerima Memory<T> dan mengembalikan void, Anda tidak boleh menggunakan instans Memory<T> setelah metode Anda selesai.

Ini berkaitan dengan konsep "sewa" yang disebutkan sebelumnya. Masa pakai metode yang mengembalikan void pada instans Memory<T> dimulai ketika metode dipanggil, dan berakhir ketika metode selesai atau mengembalikan nilai. Pertimbangkan contoh berikut, yang memanggil Log dalam perulangan berdasarkan input dari konsol.

// <Snippet1>
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)
            {
                string? s = Console.ReadLine();

                if (s is null)
                    return;

                int value = Int32.Parse(s);
                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;
    }
}
// </Snippet1>

// Possible implementation of Log:
    // private static void Log(ReadOnlyMemory<char> message)
    // {
    //     Console.WriteLine(message);
    // }

Jika Log merupakan metode yang sepenuhnya sinkron, kode ini akan bersifat seperti yang diharapkan karena hanya ada satu konsumen aktif instans memori pada waktu tertentu. Bayangkan jika Log memiliki implementasi ini.

// !!! 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);
    });
}

Dalam implementasi ini, Log melanggar kontraknya karena masih mencoba menggunakan instance Memory<T> di latar belakang setelah metode asli selesai dijalankan. Metode ini Main dapat mengubah buffer saat Log mencoba membaca darinya, yang dapat mengakibatkan kerusakan data.

Ada beberapa cara untuk mengatasi masalah ini:

  • Metode Log dapat mengembalikan Task alih-alih void, seperti yang dilakukan oleh implementasi metode Log berikut.

    // 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 dapat diimplementasikan sebagai berikut:

    // 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();
        });
    }
    

Aturan #4: Jika metode Anda menerima Memori<T> dan mengembalikan Tugas, Anda tidak boleh menggunakan instans Memory<T> setelah Tugas beralih ke status terminal

Ini hanya varian asinkron dari Aturan #3. Metode Log dari contoh sebelumnya dapat ditulis sebagai berikut untuk mematuhi aturan ini:

// 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();
    });
}

Di sini, "status terminal" berarti bahwa tugas beralih ke status selesai, rusak, atau dibatalkan. Dengan kata lain, "status terminal" berarti "apa pun yang akan menyebabkan menunggu untuk melemparkan atau melanjutkan eksekusi."

Panduan ini berlaku untuk metode yang mengembalikan Task, , Task<TResult>ValueTask<TResult>, atau jenis serupa.

Aturan #5: Jika konstruktor Anda menerima Memory<T> sebagai parameter, metode instans pada objek yang dibangun diasumsikan sebagai konsumen instans Memory<T>

Pertimbangkan contoh berikut:

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

Di sini, OddValueExtractor konstruktor menerima ReadOnlyMemory<int> sebagai parameter konstruktor, sehingga konstruktor itu sendiri adalah pengguna instans ReadOnlyMemory<int>, dan semua metode instans pada nilai yang dikembalikan oleh konstruktor juga merupakan pengguna instans asli ReadOnlyMemory<int>. Ini berarti bahwa TryReadNextOddValue memanfaatkan instans ReadOnlyMemory<int>, meskipun instans tersebut tidak diteruskan langsung ke metode TryReadNextOddValue.

Aturan #6: Jika Anda memiliki properti bertipe Memory<T> yang dapat diatur (atau metode instans yang setara) pada tipe Anda, metode instans pada objek tersebut diasumsikan sebagai konsumen instans Memory<T>.

Ini benar-benar hanya varian Aturan #5. Aturan ini ada karena setter properti atau metode yang setara diasumsikan untuk menangkap dan mempertahankan inputnya, sehingga metode instans pada objek yang sama dapat menggunakan status yang ditangkap.

Contoh berikut memicu aturan ini:

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

Aturan #7: Jika Anda memiliki referensi IMemoryOwner<T> , Anda harus membuangnya atau mentransfer kepemilikannya (tetapi tidak keduanya)

Karena instans Memory<T> dapat didukung oleh memori terkelola atau tidak terkelola, pemilik harus memanggil Dispose pada IMemoryOwner<T> saat pekerjaan yang dilakukan pada instans Memory<T> selesai. Atau, pemilik dapat mentransfer kepemilikan IMemoryOwner<T> instans ke komponen yang berbeda, di mana komponen yang memperoleh bertanggung jawab untuk memanggil Dispose pada waktu yang tepat (lebih lanjut tentang ini nanti).

Kegagalan untuk memanggil Dispose metode pada IMemoryOwner<T> instans dapat menyebabkan kebocoran memori yang tidak dikelola atau penurunan performa lainnya.

Aturan ini juga berlaku untuk kode yang memanggil metode pabrik seperti MemoryPool<T>.Rent. Pemanggil menjadi pemilik yang menerima pengembalian IMemoryOwner<T> dan bertanggung jawab untuk menghapus instans tersebut setelah selesai.

Aturan #8: Jika Anda memiliki parameter IMemoryOwner<T> di permukaan API, Anda menerima kepemilikan instans tersebut

Menerima instans jenis ini memberi sinyal bahwa komponen Anda ingin mengambil kepemilikan instans ini. Komponen Anda bertanggung jawab memastikan pembuangan dilakukan sesuai Aturan #7.

Komponen apa pun yang mentransfer kepemilikan IMemoryOwner<T> instans ke komponen yang berbeda tidak boleh lagi menggunakan instans tersebut setelah panggilan metode selesai.

Penting

Jika konstruktor Anda menerima IMemoryOwner<T> sebagai parameter, jenisnya harus mengimplementasikan IDisposable, dan metode Dispose Anda harus memanggil Dispose pada objek IMemoryOwner<T>.

Aturan #9: Jika Anda membungkus metode p/invoke yang sinkron, API Anda harus dapat menerima Span<T> sebagai parameter

Menurut Aturan #1, Span<T> umumnya adalah jenis yang benar untuk digunakan untuk API sinkron. Anda dapat menyematkan instans Span<T> dengan menggunakan kata kunci fixed, seperti dalam contoh berikut.

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

Dalam contoh sebelumnya, pbData dapat null jika, misalnya, rentang input kosong. Jika metode yang diekspor benar-benar mengharuskan yang pbData non-null, bahkan jika cbData adalah 0, metode dapat diimplementasikan sebagai berikut:

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

Aturan #10: Jika Anda membungkus metode p/invoke asinkron, API Anda harus menerima Memory<T> sebagai parameter

Karena Anda tidak dapat menggunakan kata kunci fixed dalam operasi asinkron, Anda menggunakan metode Memory<T>.Pin untuk menyematkan instans Memory<T>, terlepas dari jenis memori bersebelahan yang diwakili instans. Contoh berikut menunjukkan cara menggunakan API ini untuk melakukan panggilan p/invoke asinkron.

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

Lihat juga