Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
.NET inclut un certain nombre de types qui représentent une région contiguë arbitraire de la mémoire. Span<T> et ReadOnlySpan<T> sont des mémoires tampons légères qui encapsulent les références à la mémoire managée ou non managée. Étant donné que ces types ne peuvent être stockés que sur la pile, ils ne conviennent pas aux scénarios tels que les appels de méthode asynchrone. Pour résoudre ce problème, .NET 2.1 a ajouté certains types supplémentaires, notamment Memory<T>, , ReadOnlyMemory<T>IMemoryOwner<T>et MemoryPool<T>. Comme Span<T>, Memory<T> et ses types associés peuvent être sauvegardés par la mémoire managée et non managée. Contrairement à Span<T>, Memory<T> peut être stockée sur le tas managé.
Span<T> et Memory<T> sont des wrappers sur des mémoires tampons de données structurées qui peuvent être utilisées dans les pipelines. Autrement dit, ils sont conçus pour que certaines ou toutes les données puissent être transmises efficacement aux composants du pipeline, ce qui peut les traiter et éventuellement modifier la mémoire tampon. Étant donné que Memory<T> et ses types associés sont accessibles par plusieurs composants ou par plusieurs threads, il est important de suivre certaines instructions d’utilisation standard pour produire du code robuste.
Propriétaires, consommateurs et gestion de la durée de vie
Les mémoires tampons peuvent être transmises entre les API et sont parfois accessibles à partir de plusieurs threads. Sachez donc comment la durée de vie d’une mémoire tampon est gérée. Il existe trois concepts fondamentaux :
Propriété. Le propriétaire d’une instance de mémoire tampon est responsable de la gestion de la durée de vie, y compris la destruction de la mémoire tampon lorsqu’elle n’est plus utilisée. Toutes les mémoires tampons ont un seul propriétaire. En règle générale, le propriétaire est le composant qui a créé la mémoire tampon ou qui a reçu la mémoire tampon d’une fabrique. La propriété peut également être transférée ; Le composant A peut abandonner le contrôle de la mémoire tampon à Component-B, auquel point Component-A peut ne plus utiliser la mémoire tampon, et Component-B devient responsable de la destruction de la mémoire tampon lorsqu’elle n’est plus utilisée.
Consommation. Le consommateur d’une instance de mémoire tampon est autorisé à utiliser l’instance de mémoire tampon en la lisant et en écrivant éventuellement dans celle-ci. Les mémoires tampons peuvent avoir un consommateur à la fois, sauf si un mécanisme de synchronisation externe est fourni. Le consommateur actif d’une mémoire tampon n’est pas nécessairement le propriétaire de la mémoire tampon.
Bail. Le bail est la durée pendant laquelle un composant particulier est autorisé à être le consommateur de la mémoire tampon.
L’exemple de pseudo-code suivant illustre ces trois concepts.
Buffer
dans le pseudo-code représente un Memory<T> ou un Span<T> tampon de type Char. La Main
méthode instancie la mémoire tampon, appelle la WriteInt32ToBuffer
méthode pour écrire la représentation sous forme de chaîne d’un entier dans la mémoire tampon, puis appelle la DisplayBufferToConsole
méthode pour afficher la valeur de la mémoire tampon.
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();
}
}
}
La Main
méthode crée la mémoire tampon et est donc son propriétaire. Par conséquent, Main
est responsable de la destruction de la mémoire tampon lorsqu’elle n’est plus utilisée. Le pseudo-code illustre cela en appelant une Destroy
méthode sur la mémoire tampon. (Ni Memory<T> ni Span<T> n’a réellement de Destroy
méthode. Vous verrez des exemples de code réels plus loin dans cet article.)
La mémoire tampon a deux consommateurs, à savoir WriteInt32ToBuffer
et DisplayBufferToConsole
. Il n’y a qu’un seul consommateur à la fois (d’abord WriteInt32ToBuffer
, puis DisplayBufferToConsole
), et aucun des consommateurs ne possède la mémoire tampon. Notez également que le « consommateur » dans ce contexte n’implique pas une vue en lecture seule de la mémoire tampon ; les consommateurs peuvent modifier le contenu de la mémoire tampon, comme WriteInt32ToBuffer
c’est le cas, si une vue en lecture/écriture de la mémoire tampon est donnée.
La méthode WriteInt32ToBuffer
a un droit d'accès à (peut consommer) la mémoire tampon pendant la durée de l'appel de la méthode et jusqu'à son retour. De même, DisplayBufferToConsole
a un bail pour la mémoire tampon pendant son exécution et en est libéré lorsque la méthode se déroule. (Il n’existe aucune API pour la gestion des baux ; un « bail » est une question conceptuelle.)
Mémoire<T> et modèle propriétaire/consommateur
Comme mentionné dans la section Propriétaires, consommateurs et gestion de la durée de vie, une mémoire tampon a toujours un propriétaire. .NET prend en charge deux modèles de propriété :
- Modèle qui prend en charge une propriété unique. Une mémoire tampon a un seul propriétaire pour toute sa durée de vie.
- Modèle qui prend en charge le transfert de propriété. La propriété d’une mémoire tampon peut être transférée de son propriétaire d’origine (son créateur) vers un autre composant, qui devient ensuite responsable de la gestion de la durée de vie de la mémoire tampon. Ce propriétaire peut à son tour transférer la propriété vers un autre composant, et ainsi de suite.
Vous utilisez l’interface System.Buffers.IMemoryOwner<T> pour gérer explicitement la propriété d’une mémoire tampon. IMemoryOwner<T> prend en charge les deux modèles de propriété. Le composant qui possède une IMemoryOwner<T> référence possède la mémoire tampon. L’exemple suivant utilise une IMemoryOwner<T> instance pour refléter la propriété d’une Memory<T> mémoire tampon.
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}'");
}
Vous pouvez également écrire cet exemple avec l’instructionusing
:
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}'");
}
Dans ce code :
- La
Main
méthode contient la référence à l’instance IMemoryOwner<T> , de sorte que laMain
méthode est le propriétaire de la mémoire tampon. - Les méthodes
WriteInt32ToBuffer
etDisplayBufferToConsole
acceptent Memory<T> en tant qu'API publique. Par conséquent, ils sont des consommateurs de la mémoire tampon. Ces méthodes consomment la mémoire tampon une à la fois.
Bien que la WriteInt32ToBuffer
méthode soit destinée à écrire une valeur dans la mémoire tampon, la DisplayBufferToConsole
méthode n’est pas destinée. Pour refléter cela, il aurait pu accepter un argument de type ReadOnlyMemory<T>. Pour plus d’informations sur ReadOnlyMemory<T>, consultez la règle n° 2 : Utilisez ReadOnlySpan<T> ou ReadOnlyMemory<T> si la mémoire tampon doit être en lecture seule.
Instances mémoire<T> « sans propriétaire »
Vous pouvez créer une Memory<T> instance sans utiliser IMemoryOwner<T>. Dans ce cas, la propriété de la mémoire tampon est implicite plutôt que explicite, et seul le modèle propriétaire unique est pris en charge. Pour ce faire, vous pouvez :
- Appeler directement un des constructeurs Memory<T> en passant en entrée un
T[]
, comme dans l’exemple suivant. - Appel de la méthode d’extension String.AsMemory pour produire une
ReadOnlyMemory<char>
instance.
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}'");
}
La méthode qui crée initialement l’instance Memory<T> est le propriétaire implicite de la mémoire tampon. La propriété ne peut pas être transférée vers un autre composant, car il n’existe aucune IMemoryOwner<T> instance pour faciliter le transfert. (Comme alternative, vous pouvez également imaginer que le récupérateur de mémoire du runtime possède la mémoire tampon et que toutes les méthodes ne fassent que consommer la mémoire tampon.)
Instructions d’utilisation
Étant donné qu’un bloc de mémoire est détenu mais qu’il est destiné à être passé à plusieurs composants, dont certains peuvent fonctionner simultanément sur un bloc de mémoire particulier, il est important d’établir des instructions pour l’utilisation à la fois Memory<T> et Span<T>. Les instructions sont nécessaires, car il est possible qu’un composant effectue les étapes suivantes :
- Conservez une référence à un bloc de mémoire une fois que son propriétaire l’a libéré.
- Opérez sur une mémoire tampon en même temps qu'un autre composant opère dessus, entraînant la corruption des données de la mémoire tampon.
Alors que l’allocation par pile de Span<T> optimise les performances et fait de Span<T> le type préféré de fonctionnement sur un bloc de mémoire, elle soumet également Span<T> à certaines restrictions majeures. Il est important de savoir quand utiliser un Span<T> et quand utiliser Memory<T>.
Voici nos recommandations relatives à l’utilisation Memory<T> et à ses types associés. Les directives qui s’appliquent à Memory<T> et Span<T> s’appliquent également à ReadOnlyMemory<T> et ReadOnlySpan<T> sauf indication contraire.
-
Règle n°1 : Pour une API synchrone, utilisez
Span<T>
plutôtMemory<T>
que comme paramètre si possible. -
Règle n° 2 : Utiliser
ReadOnlySpan<T>
ouReadOnlyMemory<T>
si la mémoire tampon doit être en lecture seule -
Règle n° 3 : Si votre méthode accepte
Memory<T>
et retournevoid
, vous ne devez pas utiliser l’instanceMemory<T>
une fois votre méthode retournée -
Règle n°4 : Si votre méthode accepte une
Memory<T>
tâche et retourne une tâche, vous ne devez pas utiliser l’instance après la transition de laMemory<T>
tâche vers un état terminal -
Règle 5 : Si votre constructeur accepte
Memory<T>
comme paramètre, les méthodes d’instance sur l’objet construit sont supposées être des consommateurs de l’instanceMemory<T>
. -
Règle n°6 : Si vous avez une propriété définissable de type
Memory<T>
(ou une méthode d’instance équivalente) sur votre type, les méthodes d’instance sur cet objet sont supposées être des consommateurs de l’instanceMemory<T>
-
Règle n°7 : Si vous avez une
IMemoryOwner<T>
référence, vous devez à un moment donné le supprimer ou transférer sa propriété (mais pas les deux) -
Règle n° 8 : Si vous avez un
IMemoryOwner<T>
paramètre dans votre surface d’API, vous acceptez la propriété de cette instance. -
Règle n° 9 : Si vous enveloppez une méthode P/Invoke synchrone, l'API devrait accepter
Span<T>
comme paramètre -
Règle n° 10 : Si vous encapsulez une méthode p/invoke asynchrone, votre API doit accepter
Memory<T>
comme paramètre
Règle n°1 : Pour une API synchrone, utilisez span<T> au lieu de mémoire<T> comme paramètre si possible
Span<T> est plus polyvalent que Memory<T> et peut représenter une plus grande variété de mémoires tampons contiguës. Span<T> offre également de meilleures performances que Memory<T>. Enfin, vous pouvez utiliser la propriété Memory<T>.Span pour convertir une instance Memory<T> en une Span<T>, bien que la conversion de Span<T> à Memory<T> ne soit pas possible. Par conséquent, si vos appelants ont une Memory<T> instance, ils pourront appeler vos méthodes avec Span<T> paramètres de toute façon.
L’utilisation d’un paramètre de type Span<T> au lieu de type Memory<T> vous permet également d’écrire une implémentation de méthode consommatrice correcte. Vous obtiendrez automatiquement des vérifications au moment de la compilation pour vous assurer que vous ne tentez pas d’accéder à la mémoire tampon au-delà du bail de votre méthode (plus loin dans ce cas).
Parfois, vous devrez utiliser un Memory<T> paramètre au lieu d’un Span<T> paramètre, même si vous êtes entièrement synchrone. Peut-être qu'une API dont vous dépendez n'accepte que des arguments Memory<T>. C’est bien, mais tenez compte des compromis impliqués par l’utilisation synchrone de Memory<T>.
Règle 2 : Utiliser ReadOnlySpan<T> ou ReadOnlyMemory<T> si la mémoire tampon doit être en lecture seule
Dans les exemples précédents, la DisplayBufferToConsole
méthode lit uniquement à partir de la mémoire tampon ; elle ne modifie pas le contenu de la mémoire tampon. La signature de méthode doit être remplacée par ce qui suit.
void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);
En fait, si vous combinez cette règle et la règle n° 1, nous pouvons faire encore mieux et réécrire la signature de méthode comme suit :
void DisplayBufferToConsole(ReadOnlySpan<char> buffer);
La DisplayBufferToConsole
méthode fonctionne désormais avec pratiquement tous les types de mémoires tampons imaginables : T[]
, stockage alloué avec stackalloc, et ainsi de suite. Vous pouvez même passer un String directement dans celui-ci ! Pour plus d’informations, consultez le problème GitHub dotnet/docs #25551.
Règle n° 3 : Si votre méthode accepte Memory<T> et retourne void
, vous ne devez pas utiliser l’instance Memory<T> une fois que votre méthode a été exécutée.
Cela concerne le concept de « bail » mentionné précédemment. Un bail de méthode avec renvoi d’annulation sur l’instance Memory<T> commence lorsqu’on entre dans la méthode et se termine lorsqu’on la quitte. Prenons l’exemple suivant, qui appelle Log
dans une boucle en fonction de l’entrée de la console.
// <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);
// }
S’il Log
s’agit d’une méthode entièrement synchrone, ce code se comporte comme prévu, car il n’existe qu’un seul consommateur actif de l’instance de mémoire à un moment donné.
Mais imaginez plutôt que Log
a cette implémentation.
// !!! 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);
});
}
Dans cette implémentation, Log
enfreint son bail, car il tente toujours d’utiliser l’instance Memory<T> en arrière-plan une fois la méthode d’origine retournée. La Main
méthode peut muter la mémoire tampon lors Log
des tentatives de lecture à partir de celle-ci, ce qui peut entraîner une altération des données.
Il y a de nombreuses manières de résoudre ce problème :
La méthode
Log
peut renvoyer une Task au lieu d'unevoid
, comme le fait l'implémentation suivante de la méthodeLog
.// 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
peut plutôt être implémenté comme suit :// 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(); }); }
Règle n° 4 : Si votre méthode accepte une<mémoire T> et retourne une tâche, vous ne devez pas utiliser l’instance Memory<T> après la transition de la tâche vers un état terminal
Il s’agit simplement de la variante asynchrone de la règle #3. La Log
méthode de l’exemple précédent peut être écrite comme suit pour se conformer à cette règle :
// 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();
});
}
Ici, « état terminal » signifie que la tâche passe à un état terminé, défectueux ou annulé. En d’autres termes, « état terminal » signifie « tout ce qui provoquerait une attente lors du lancement ou de la poursuite de l’exécution. »
Cette aide s’applique aux méthodes qui retournent Task, Task<TResult>, ValueTask<TResult> ou tout type similaire.
Règle n° 5 : Si votre constructeur accepte memory<T> comme paramètre, les méthodes d’instance sur l’objet construit sont supposées être des consommateurs de l’instance Memory<T>
Prenons l’exemple suivant :
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);
}
}
Ici, le OddValueExtractor
constructeur accepte un ReadOnlyMemory<int>
paramètre de constructeur, de sorte que le constructeur lui-même est un consommateur de l’instance ReadOnlyMemory<int>
, et toutes les méthodes d’instance sur la valeur retournée sont également des consommateurs de l’instance d’origine ReadOnlyMemory<int>
. Cela signifie qu’elle TryReadNextOddValue
consomme l’instance ReadOnlyMemory<int>
, même si l’instance n’est pas passée directement à la TryReadNextOddValue
méthode.
Règle n°6 : Si vous avez une propriété de type Memory<T> configurable (ou une méthode d’instance équivalente) sur votre type, on suppose que les méthodes d’instance de cet objet consomment l’instance Memory<T>.
Ce n’est vraiment qu’une variante de la règle n° 5. Cette règle existe, car les setters de propriétés ou les méthodes équivalentes sont supposées capturer et conserver leurs entrées. Par conséquent, les méthodes d’instance sur le même objet peuvent utiliser l’état capturé.
L’exemple suivant déclenche cette règle :
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;
}
Règle 7 : Si vous disposez d’une référence IMemoryOwner<T> , vous devez à un moment donné le supprimer ou transférer sa propriété (mais pas les deux)
Étant donné qu’une Memory<T> instance peut être sauvegardée par une mémoire managée ou non managée, le propriétaire doit appeler Dispose
IMemoryOwner<T> lorsque le travail effectué sur l’instance Memory<T> est terminé. Le propriétaire peut également transférer la propriété de l’instance IMemoryOwner<T> vers un autre composant, auquel cas le composant d’acquisition devient responsable de l’appel Dispose
au moment approprié (plus tard).
L’échec de l’appel de la Dispose
méthode sur une IMemoryOwner<T> instance peut entraîner des fuites de mémoire non managées ou une autre dégradation des performances.
Cette règle s’applique également au code qui appelle des méthodes de fabrique comme MemoryPool<T>.Rent. L’appelant devient le propriétaire du IMemoryOwner<T> retourné et est responsable de la suppression de l’instance terminée.
Règle n° 8 : Si vous avez un paramètre IMemoryOwner<T> dans votre surface d’API, vous acceptez la propriété de cette instance.
L’acceptation d’une instance de ce type signale que votre composant a l’intention de prendre possession de cette instance. Votre composant devient responsable de l’élimination appropriée conformément à la règle 7.
Tout composant qui transfère la propriété de l’instance IMemoryOwner<T> à un autre composant ne doit plus utiliser cette instance une fois l’appel de méthode terminé.
Importante
Si votre constructeur accepte IMemoryOwner<T> comme paramètre, son type doit implémenter IDisposableet votre Dispose méthode doit appeler Dispose
l’objet IMemoryOwner<T> .
Règle n°9 : Si vous encapsulez une méthode p/invoke synchrone, votre API doit accepter Span<T> comme paramètre
Selon la règle n°1, Span<T> est généralement le type approprié à utiliser pour les API synchrones. Vous pouvez épingler des instances Span<T> via le mot clé fixed
mot clé, comme dans l’exemple suivant.
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;
}
}
Dans l’exemple précédent, pbData
peut être null si, par exemple, l’étendue d’entrée est vide. Si la méthode exportée nécessite absolument qu’elle pbData
ne soit pas null, même si cbData
elle est égale à 0, la méthode peut être implémentée comme suit :
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;
}
}
Règle n° 10 : Si vous encapsulez une méthode p/invoke asynchrone, votre API doit accepter Memory<T> comme paramètre.
Étant donné que vous ne pouvez pas utiliser le fixed
mot clé entre les opérations asynchrones, vous utilisez la Memory<T>.Pin méthode pour épingler Memory<T> des instances, quel que soit le type de mémoire contiguë que représente l’instance. L’exemple suivant montre comment utiliser cette API pour effectuer un appel p/invoke asynchrone.
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;
}