Panduan penggunaan Memory<T> dan Span<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 jenisnya yang terkait dapat didukung oleh memori terkelola dan tidak terkelola. Tidak seperti Span<T>, Memory<T> dapat disimpan di heap terkelola.

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

Pemilik, konsumen, dan pengelolaan 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 pengelolaan seumur hidup, termasuk menghancurkan buffer saat tidak digunakan lagi. 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 menggunakan buffer lagi, dan Component-B menjadi bertanggung jawab untuk menghancurkan buffer saat tidak digunakan lagi.

  • Konsumsi. Konsumen instans buffer boleh menggunakan instans buffer dengan membacanya dan mungkin menulisnya. Buffer dapat memiliki satu konsumen pada satu waktu kecuali beberapa mekanisme sinkronisasi eksternal disediakan. Konsumen buffer yang aktif belum tentu pemilik buffer.

  • Penyewaan. Penyewaan adalah lamanya waktu komponen tertentu diizinkan untuk menjadi konsumen buffer.

Contoh kode semu berikut menggambarkan ketiga konsep ini. Buffer dalam kode semu mewakili Memory<T> atau Span<T> buffer jenis Char. Metode Main membuat instans buffer, memanggil metode WriteInt32ToBuffer untuk menulis representasi string bilangan bulat ke buffer, lalu 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 membuat buffer dan begitu juga pemiliknya. Oleh karena itu, Main bertanggung jawab untuk menghancurkan buffer saat tidak digunakan lagi. Kode semu menggambarkan ini dengan memanggil metode Destroy pada buffer. (Baik Memory<T> maupun Span<T> sebenarnya tidak memiliki metode Destroy. Anda akan melihat contoh kode yang sebenarnya nanti di artikel ini.)

Buffer memiliki dua konsumen, WriteInt32ToBuffer dan DisplayBufferToConsole. Hanya ada satu konsumen pada satu waktu (pertama WriteInt32ToBuffer, lalu DisplayBufferToConsole), dan tidak ada konsumen yang memiliki buffer. Harap diingat juga bahwa "konsumen" dalam konteks ini tidak menyiratkan tampilan buffer baca saja; konsumen dapat mengubah konten buffer, seperti halnya WriteInt32ToBuffer, jika diberikan tampilan baca/tulis buffer.

Metode WriteInt32ToBuffer memiliki penyewaan (dapat menggunakan) buffer antara awal pemanggilan metode dan waktu metode kembali. Demikian pula, DisplayBufferToConsole memiliki penyewaan buffer saat dijalankan, dan penyewaan dirilis saat metode terlepas. (Tidak ada API untuk pengelolaan penyewaan; "penyewaan" adalah masalah konseptual.)

Memory<T> dan model pemilik/konsumen

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

  • Model yang mendukung kepemilikan tunggal. Buffer memiliki satu pemilik untuk seluruh masa pakainya.

  • Model yang mendukung transfer kepemilikan. Kepemilikan buffer dapat ditransfer dari pemilik aslinya (pembuatnya) ke komponen lain, yang kemudian menjadi bertanggung jawab atas pengelolaan seumur hidup buffer. Pemilik itu secara bergiliran dapat mentransfer kepemilikan ke komponen lain, dan seterusnya.

Anda menggunakan antarmuka System.Buffers.IMemoryOwner<T> untuk mengelola kepemilikan buffer secara eksplisit. IMemoryOwner<T> mendukung kedua model kepemilikan. Komponen yang memiliki referensi IMemoryOwner<T> memiliki buffer. Contoh berikut menggunakan instans IMemoryOwner<T> untuk mencerminkan kepemilikan buffer 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
        {
            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}'");
}

Kita 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 instans IMemoryOwner<T>, jadi metode Main adalah pemilik buffer.

  • Metode WriteInt32ToBuffer dan DisplayBufferToConsole menerima Memory<T> sebagai API publik. Oleh karena itu, mereka adalah konsumen buffer. Metode ini mengonsumsi buffer satu per satu.

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

Instans Memory<T> "Tanpa Pemilik"

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

  • Memanggil salah satu konstruktor Memory<T> secara langsung, meneruskan T[], seperti yang dilakukan contoh berikut.

  • Memanggil metode ekstensi String.AsMemory untuk menghasilkan instans ReadOnlyMemory<char>.

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 instans Memory<T> adalah pemilik implisit buffer. Kepemilikan tidak dapat ditransfer ke komponen lain karena tidak ada instans IMemoryOwner<T> untuk memfasilitasi transfer. (Sebagai alternatif, Anda juga dapat membayangkan bahwa pengumpul sampah runtime memiliki buffer, dan semua metode hanya menggunakan buffer.)

Panduan penggunaan

Karena blok memori dimiliki tetapi dimaksudkan untuk diteruskan ke beberapa komponen, beberapa di antaranya dapat beroperasi pada blok memori tertentu secara bersamaan, penting untuk menetapkan panduan untuk menggunakan dan Memory<T>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 dari Span<T> mengoptimalkan performa dan menjadikan Span<T> jenis yang disukai untuk beroperasi pada blok memori, sifat ini juga tunduk Span<T> pada beberapa batasan utama. Penting untuk mengetahui kapan harus menggunakan Span<T> dan kapan harus menggunakan Memory<T>.

Berikut ini adalah rekomendasi kami agar berhasil menggunakan Memory<T> dan jenisnya yang terkait. 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> dan bukan Memory<T> sebagai parameter jika memungkinkan.

Span<T> lebih fleksibel daripada Memory<T> dan dapat mewakili lebih banyak variasi buffer memori yang berdekatan. Span<T> juga menawarkan performa yang lebih baik dari Memory<T>. Terakhir, Anda dapat menggunakan properti Memory<T>.Span untuk mengonversi instans Memory<T> menjadi Span<T>, meskipun konversi Span<T>-to-Memory<T> tidak dapat dilakukan. Jadi, jika pemanggil Anda memiliki instans Memory<T>, mereka tetap dapat memanggil metode Anda dengan parameter Span<T>.

Menggunakan parameter jenis Span<T> dan bukan jenis Memory<T> juga membantu Anda menulis implementasi metode penggunaan yang benar. Anda akan secara otomatis mendapatkan pemeriksaan waktu kompilasi untuk memastikan bahwa Anda tidak mencoba mengakses buffer di luar penyewaan metode Anda (selengkapnya tentang ini nanti).

Terkadang, Anda harus menggunakan parameter Memory<T> dan bukan parameter Span<T>, meskipun Anda sepenuhnya sinkron. Mungkin API yang Anda andalkan hanya menerima argumen Memory<T>. Ini tidak masalah, tetapi waspadai pertukaran yang terlibat saat menggunakan Memory<T> secara sinkron.

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

Pada contoh sebelumnya, metode DisplayBufferToConsole hanya membaca dari buffer; metode tersebut tidak mengubah konten buffer. Tanda tangan metode harus diubah ke yang berikut ini.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

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

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

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

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

Hal ini berkaitan dengan konsep "penyewaan" yang telah disebutkan sebelumnya. Penyewaan metode pengembalian void pada instans Memory<T> dimulai saat metode dimasukkan, dan berakhir saat metode keluar. Pertimbangkan contoh berikut, yang memanggil Log secara berulang berdasarkan input dari konsol.

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

Jika Log adalah metode yang sepenuhnya sinkron, kode ini akan berperilaku seperti yang diharapkan karena hanya ada satu konsumen instans memori yang aktif pada waktu tertentu. Tapi bayangkan bahwa 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 penyewaannya karena masih mencoba menggunakan instans Memory<T> di latar belakang setelah metode asli kembali. Metode ini Main dapat mengubah buffer saat Log mencoba membaca darinya, yang dapat mengakibatkan kerusakan data.

Ada beberapa cara untuk mengatasi masalah ini:

  • Metode ini Log dapat menampilkan Task dan bukan void, seperti 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 Memory<T> dan menampilkan 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 menampilkan atau melanjutkan eksekusi."

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

Aturan #5: Jika konstruktor Anda menerima Memory<T> sebagai parameter, metode instans pada objek yang dibuat dianggap sebagai konsumen dari 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, konstruktor OddValueExtractor menerima ReadOnlyMemory<int> sebagai parameter konstruktor, sehingga konstruktor itu sendiri adalah konsumen instans ReadOnlyMemory<int>, dan semua metode instans pada nilai yang ditampilkan juga merupakan konsumen instans ReadOnlyMemory<int> asli. Ini berarti bahwa TryReadNextOddValue menggunakan instans, meskipun instans ReadOnlyMemory<int> tidak diteruskan langsung ke metode TryReadNextOddValue.

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

Ini benar-benar hanya varian Aturan #5. Aturan ini ada karena pengatur properti atau metode yang setara diasumsikan mengambil dan mempertahankan input mereka, sehingga metode instans pada objek yang sama dapat memanfaatkan status yang diambil.

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 di IMemoryOwner<T> saat pekerjaan yang dilakukan pada instans Memory<T> selesai. Atau, pemilik dapat mentransfer kepemilikan instans IMemoryOwner<T> ke komponen lain, di mana komponen yang memperoleh menjadi bertanggung jawab untuk memanggil Dispose pada waktu yang tepat (selengkapnya tentang ini nanti).

Kegagalan memanggil metode Dispose pada instans IMemoryOwner<T> 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 IMemoryOwner<T> yang ditampilkan dan bertanggung jawab untuk membuang instans setelah selesai.

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

Menerima instans jenis ini menandakan bahwa komponen Anda bermaksud untuk mengambil kepemilikan atas instans ini. Komponen Anda bertanggung jawab atas pembuangan yang tepat sesuai dengan Aturan #7.

Setiap komponen yang mentransfer kepemilikan instans IMemoryOwner<T> ke komponen lain tidak boleh menggunakan instans itu lagi setelah panggilan metode selesai.

Penting

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

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

Berdasarkan Aturan #1, Span<T> umumnya adalah jenis yang benar untuk digunakan untuk API sinkron. Anda dapat menyematkan instans Span<T> melalui 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 pbData non-null, meskipun 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 di seluruh operasi asinkron, Anda menggunakan metode Memory<T>.Pin untuk menyematkan instans Memory<T>, terlepas dari jenis memori yang berdekatan 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