다음을 통해 공유


메모리<T> 및 Span<T> 사용 지침

.NET에는 임의 연속 메모리 영역을 나타내는 여러 형식이 포함되어 있습니다. Span<T> 관리되거나 ReadOnlySpan<T> 관리되지 않는 메모리에 대한 참조를 래핑하는 경량 메모리 버퍼입니다. 이러한 형식은 스택에만 저장할 수 있으므로 비동기 메서드 호출과 같은 시나리오에는 적합하지 않습니다. 이 문제를 해결하기 위해 .NET 2.1에는 , , 및 Memory<T>를 비롯한 ReadOnlyMemory<T>몇 가지 추가 형식이 IMemoryOwner<T>추가MemoryPool<T>되었습니다. 마찬가지로 Span<T>관련 Memory<T> 형식은 관리되는 메모리와 관리되지 않는 메모리 모두에서 지원될 수 있습니다. 달리 Span<T>관리되는 Memory<T> 힙에 저장할 수 있습니다.

Span<T> 둘 다 Memory<T> 파이프라인에서 사용할 수 있는 구조화된 데이터의 버퍼에 대한 래퍼입니다. 즉, 일부 또는 모든 데이터를 파이프라인의 구성 요소에 효율적으로 전달할 수 있도록 설계되어 이를 처리하고 필요에 따라 버퍼를 수정할 수 있습니다. 여러 구성 요소 또는 여러 스레드에서 관련 형식에 액세스할 수 있으므로 Memory<T> 강력한 코드를 생성하려면 몇 가지 표준 사용 지침을 따르는 것이 중요합니다.

소유자, 소비자 및 수명 관리

버퍼는 API 간에 전달될 수 있으며 때로는 여러 스레드에서 액세스할 수 있으므로 버퍼의 수명이 어떻게 관리되는지 알고 있어야 합니다. 세 가지 핵심 개념이 있습니다.

  • 소유권. 버퍼 인스턴스의 소유자는 더 이상 사용되지 않을 때 버퍼를 삭제하는 것을 포함하여 수명 관리를 담당합니다. 모든 버퍼에는 단일 소유자가 있습니다. 일반적으로 소유자는 버퍼를 만들거나 팩터리에서 버퍼를 받은 구성 요소입니다. 소유권을 이전할 수도 있습니다. Component-A 는 버퍼의 제어를 Component-B로 포기할 수 있으며, 이때 Component-A 는 더 이상 버퍼를 사용하지 않을 수 있으며, Component-B 는 더 이상 사용되지 않을 때 버퍼를 삭제합니다.

  • 소비량. 버퍼 인스턴스의 소비자는 버퍼 인스턴스에서 읽고 쓸 수 있도록 버퍼 인스턴스를 사용할 수 있습니다. 일부 외부 동기화 메커니즘이 제공되지 않는 한 버퍼는 한 번에 하나의 소비자를 가질 수 있습니다. 버퍼의 활성 소비자가 반드시 버퍼의 소유자가 아닌 것은 아닙니다.

  • 임대. 임대는 특정 구성 요소가 버퍼의 소비자가 될 수 있는 시간입니다.

다음 의사코드 예제에서는 이러한 세 가지 개념을 설명합니다. Buffer는 의사 코드에서 Memory<T> 또는 Span<T> 버퍼 중 Char 형식의 버퍼를 나타냅니다. 메서드는 Main 버퍼를 인스턴스화하고, 메서드를 호출 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();
        }
    }
}

메서드는 Main 버퍼를 만들고 소유자도 마찬가지입니다. 따라서 Main 버퍼가 더 이상 사용되지 않을 때 버퍼를 삭제해야 합니다. 의사 코드는 버퍼에서 메서드 Destroy를 호출함으로써 이를 설명합니다. (Memory<T>Span<T> 모두 실제로 Destroy 메서드를 가지고 있지 않습니다. 실제 코드 예제는 이 문서의 후반부에서 보게 될 것입니다.)

버퍼에는 두 명의 소비자, WriteInt32ToBufferDisplayBufferToConsole가 있습니다. 한 번에 하나의 소비자(첫 번째 WriteInt32ToBuffer, 다음 DisplayBufferToConsole)만 있으며 두 소비자 모두 버퍼를 소유하지 않습니다. 또한 이 컨텍스트의 "소비자"는 버퍼의 읽기 전용 보기를 의미하지는 않습니다. 소비자는 버퍼의 읽기/쓰기 뷰가 지정된 경우와 마찬가지로 WriteInt32ToBuffer 버퍼의 콘텐츠를 수정할 수 있습니다.

메서드는 메서드 호출이 시작되는 시점부터 메서드가 반환하기까지 버퍼를 사용할 수 있는 임대를 갖고 있습니다. 마찬가지로, DisplayBufferToConsole가 실행되는 동안 버퍼에 임대가 있으며, 메서드가 해제될 때 그 임대는 해제됩니다. (임대 관리를 위한 API는 없습니다. "임대"는 개념적 문제입니다.)

메모리<T> 및 소유자/소비자 모델

소유자, 소비자 및 수명 관리 섹션에서 설명한 것처럼 버퍼에는 항상 소유자가 있습니다. .NET은 다음 두 가지 소유권 모델을 지원합니다.

  • 단일 소유권을 지원하는 모델입니다. 버퍼에는 전체 수명 동안 단일 소유자가 있습니다.
  • 소유권 이전을 지원하는 모델입니다. 버퍼의 소유권은 원래 소유자(작성자)에서 다른 구성 요소로 이전할 수 있으며, 그러면 버퍼의 수명 관리를 담당하게 됩니다. 해당 소유자는 소유권을 다른 구성 요소로 이전할 수 있습니다.

인터페이스를 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
        {
            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}'");
}

다음 명령문을 사용하여 이 예제를 작성할 수도 있습니다.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
            {
                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}'");
}

이 코드에서는 다음을 수행합니다.

  • 메서드는 Main 인스턴스에 대한 참조를 IMemoryOwner<T> 보유하므로 Main 메서드는 버퍼의 소유자입니다.
  • WriteInt32ToBufferDisplayBufferToConsole 메서드는 Memory<T>를 공용 API로 허용합니다. 따라서 버퍼의 소비자입니다. 이러한 메서드는 버퍼를 한 번에 하나씩 소모합니다.

WriteInt32ToBuffer 메서드는 버퍼에 값을 쓰기 위한 것이지만, DisplayBufferToConsole 메서드는 그 목적이 아닙니다. 이를 반영하기 위해 형식 ReadOnlyMemory<T>의 인수를 수락했을 수 있습니다. 자세한 ReadOnlyMemory<T>내용은 규칙 #2: 버퍼가 읽기 전용이어야 하는 경우 ReadOnlySpan<T> 또는 ReadOnlyMemory<T> 를 사용합니다.

"소유자 없는" 메모리<T> 인스턴스

Memory<T> 인스턴스를 IMemoryOwner<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: ");
        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}'");
}

처음에 인스턴스를 Memory<T> 만드는 메서드는 버퍼의 암시적 소유자입니다. 이전을 용이하게 하는 인스턴스가 없으므로 소유권을 다른 구성 요소로 이전할 수 없습니다 IMemoryOwner<T> . (또는 런타임의 가비지 수집기가 버퍼를 소유하고 모든 메서드가 버퍼만 사용한다고 상상할 수도 있습니다.)

사용 지침

메모리 블록은 소유되지만 여러 구성 요소에 전달되기 위한 것이며, 그 중 일부는 특정 메모리 블록에서 동시에 작동할 수 있으므로 두 구성 요소 모두를 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의 경우 가능하면 메모리<T 대신 Span>T<>를 매개 변수로 사용합니다.

Span<T>Memory<T>보다 더 다양하며 더 많은 연속 메모리 버퍼를 나타낼 수 있습니다. Span<T> 은 .보다 Memory<T>더 나은 성능을 제공합니다. 마지막으로 Memory<T>.Span 속성을 사용하여 Memory<T> 인스턴스를 Span<T>으로 변환할 수 있습니다. 하지만 Span<T>를 Memory<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을 결합하면 다음과 같이 메서드 서명을 더 잘 수행하고 다시 작성할 수 있습니다.

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

이제 DisplayBufferToConsole 메서드는 거의 모든 버퍼 유형에서 작동합니다. 예를 들어, T[], stackalloc로 할당된 스토리지 등입니다. 직접 String를 보낼 수 있습니다! 자세한 내용은 GitHub 문제 dotnet/docs #25551을 참조하세요.

규칙 #3: 메서드가 Memory<T> 를 수락하고 반환하는 경우 메서드가 반환 void된 후 메모리<T> 인스턴스를 사용하면 안 됩니다.

이는 앞에서 언급한 "임대" 개념과 관련이 있습니다. 인스턴스에 대한 Memory<T> void 반환 메서드의 임대는 메서드가 입력될 때 시작되고 메서드가 종료될 때 종료됩니다. 콘솔의 입력을 기반으로 루프에서 호출 Log 하는 다음 예제를 고려해 보세요.

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

완전히 동기 메서드인 경우 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 에서는 원래 메서드가 반환된 후에도 백그라운드에서 인스턴스를 Memory<T> 계속 사용하려고 하기 때문에 임대를 위반합니다. 이 메서드는 Main 버퍼에서 읽으려고 시도하는 동안 Log 버퍼를 변경하여 데이터가 손상될 수 있습니다.

이 문제를 해결하는 몇 가지 방법은 다음과 같습니다.

  • Log 메서드는 Task 대신 void을 반환할 수 있으며, 다음 구현에서는 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(() => {
            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: 메서드가 메모리<T> 를 수락하고 작업을 반환하는 경우 태스크가 터미널 상태로 전환된 후 메모리<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(() => {
        StreamWriter sw = File.AppendText(@".\input-numbers.dat");
        sw.WriteLine(message);
        sw.Flush();
    });
}

여기서 "터미널 상태"는 태스크가 완료됨, 오류 또는 취소된 상태로 전환됨을 의미합니다. 즉, "터미널 상태"는 "대기를 throw하거나 실행을 계속할 수 있는 모든 것"을 의미합니다.

이 지침은 반환하는 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: 형식에 Settable Memory<T> 형식 속성(또는 해당하는 인스턴스 메서드)이 있는 경우 해당 개체의 인스턴스 메서드는 Memory<T> 인스턴스의 소비자로 간주됩니다.

이것은 실제로 규칙 #5의 변형일 뿐입니다. 이 규칙은 속성 setter 또는 해당 메서드가 입력을 캡처하고 유지하는 것으로 간주되므로 동일한 개체의 인스턴스 메서드가 캡처된 상태를 활용할 수 있기 때문입니다.

다음 예제에서는 이 규칙을 트리거합니다.

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> 인스턴스가 관리되는 메모리 또는 관리되지 않는 메모리에서 지원될 수 있으므로, Dispose 인스턴스에서 수행된 작업이 완료되면 소유자는 IMemoryOwner<T>에 대해 Memory<T>을 호출해야 합니다. 또는 소유자가 인스턴스의 IMemoryOwner<T> 소유권을 다른 구성 요소로 이전할 수 있습니다. 이때 획득 구성 요소는 적절한 시간에 호출 Dispose 을 담당하게 됩니다(나중에 자세히 설명).

인스턴스에서 Dispose 메서드를 IMemoryOwner<T> 호출하지 않으면 관리되지 않는 메모리 누수 또는 기타 성능 저하가 발생할 수 있습니다.

이 규칙은 다음과 같은 MemoryPool<T>.Rent팩터리 메서드를 호출하는 코드에도 적용됩니다. 호출자는 반환 IMemoryOwner<T> 된 소유자가 되며 완료되면 인스턴스를 삭제해야 합니다.

규칙 #8: API 화면에 IMemoryOwner<T> 매개 변수가 있는 경우 해당 인스턴스의 소유권을 수락합니다.

이 형식의 인스턴스를 수락하면 구성 요소가 이 인스턴스의 소유권을 취하려 한다는 신호가 표시됩니다. 규칙 #7에 따라 구성 요소가 적절한 폐기를 담당하게 됩니다.

인스턴스의 IMemoryOwner<T> 소유권을 다른 구성 요소로 전송하는 모든 구성 요소는 메서드 호출이 완료된 후 해당 인스턴스를 더 이상 사용하지 않아야 합니다.

중요합니다

생성자가 매개 변수로 수락 IMemoryOwner<T> 하는 경우 해당 형식이 구현IDisposable되어야 하며 Dispose 메서드가 개체를 Dispose 호출 IMemoryOwner<T> 해야 합니다.

규칙 #9: 동기 p/invoke 메서드를 래핑하는 경우 API는 Span<T> 를 매개 변수로 수락해야 합니다.

규칙 #1 Span<T> 에 따르면 일반적으로 동기 API에 사용할 올바른 형식입니다. 다음 예제와 같이 키워드를 Span<T> 통해 인스턴스를 고정 fixed 할 수 있습니다.

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

이전 예제 pbData 에서는 입력 범위가 비어 있는 경우 null일 수 있습니다. 내보낸 메서드에서 pbData이(가) null이 아닌 것이 절대적으로 필요한 경우, cbData이(가) 0이라도 메서드를 다음과 같이 구현할 수 있습니다.

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 메서드를 래핑하는 경우 API는 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;
}

참고하십시오