Interopérabilité avec d’autres types et modèles asynchrones
Bref historique des modèles asynchrones dans .NET :
- .NET Framework 1.0 a introduit le modèle IAsyncResult, également appelé Asynchronous Programming Model (APM) ou modèle
Begin/End
. - .NET Framework 2.0 a ajouté le Event-based Asynchronous Pattern (EAP).
- À partir de .NET Framework 4, le Task-based Asynchronous Pattern (TAP) remplace le modèle de programmation asychrone (APM, Asynchronous Programming Model) et le modèle asynchrone basé sur les évènements (EAP, Event-based Asynchronous Pattern), et permet de créer facilement des routines de migration à partir de modèles antérieurs.
Tâches et APM
APM vers modèle asynchrone basé sur les tâches (TAP, Task-based Asynchronous Pattern)
Étant donné que le modèle Asynchronous Programming Model (APM) est structuré, vous pouvez facilement créer un wrapper pour exposer une implémentation APM en tant qu’implémentation TAP. .NET Framework 4 et versions ultérieures incluent des routines d’assistance sous la forme de surcharges de méthode FromAsync pour fournir cette traduction.
Imaginez la classe Stream et ses méthodes BeginRead et EndRead , qui représentent la contrepartie AMP de méthode Read synchrone :
public int Read(byte[] buffer, int offset, int count)
Public Function Read(buffer As Byte(), offset As Integer,
count As Integer) As Integer
public IAsyncResult BeginRead(byte[] buffer, int offset,
int count, AsyncCallback callback,
object state)
Public Function BeginRead(buffer As Byte, offset As Integer,
count As Integer, callback As AsyncCallback,
state As Object) As IAsyncResult
public int EndRead(IAsyncResult asyncResult)
Public Function EndRead(asyncResult As IAsyncResult) As Integer
Vous pouvez utiliser la méthode TaskFactory<TResult>.FromAsync pour implémenter un wrapper TAP pour cette opération, comme le montre l’exemple ci-dessous :
public static Task<int> ReadAsync(this Stream stream,
byte[] buffer, int offset,
int count)
{
if (stream == null)
throw new ArgumentNullException("stream");
return Task<int>.Factory.FromAsync(stream.BeginRead,
stream.EndRead, buffer,
offset, count, null);
}
<Extension()>
Public Function ReadAsync(strm As Stream,
buffer As Byte(), offset As Integer,
count As Integer) As Task(Of Integer)
If strm Is Nothing Then
Throw New ArgumentNullException("stream")
End If
Return Task(Of Integer).Factory.FromAsync(AddressOf strm.BeginRead,
AddressOf strm.EndRead, buffer,
offset, count, Nothing)
End Function
Cette implémentation est similaire à ce qui suit :
public static Task<int> ReadAsync(this Stream stream,
byte [] buffer, int offset,
int count)
{
if (stream == null)
throw new ArgumentNullException("stream");
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, iar =>
{
try {
tcs.TrySetResult(stream.EndRead(iar));
}
catch(OperationCanceledException) {
tcs.TrySetCanceled();
}
catch(Exception exc) {
tcs.TrySetException(exc);
}
}, null);
return tcs.Task;
}
<Extension()>
Public Function ReadAsync(stream As Stream, buffer As Byte(), _
offset As Integer, count As Integer) _
As Task(Of Integer)
If stream Is Nothing Then
Throw New ArgumentNullException("stream")
End If
Dim tcs As New TaskCompletionSource(Of Integer)()
stream.BeginRead(buffer, offset, count,
Sub(iar)
Try
tcs.TrySetResult(stream.EndRead(iar))
Catch e As OperationCanceledException
tcs.TrySetCanceled()
Catch e As Exception
tcs.TrySetException(e)
End Try
End Sub, Nothing)
Return tcs.Task
End Function
TAP vers APM
Si votre infrastructure existante attend le modèle APM, vous voudrez également prendre une implémentation TAP et l’utiliser lorsque l’implémentation APM est attendue. Étant donné que les tâches peuvent être composées et que la classe Task implémente IAsyncResult, vous pouvez utiliser une fonction d’assistance simple pour effectuer cette opération. Le code suivant utilise une extension de la classe Task<TResult> , mais vous pouvez utiliser une fonction presque identique pour les tâches non génériques.
public static IAsyncResult AsApm<T>(this Task<T> task,
AsyncCallback callback,
object state)
{
if (task == null)
throw new ArgumentNullException("task");
var tcs = new TaskCompletionSource<T>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted)
tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(t.Result);
if (callback != null)
callback(tcs.Task);
}, TaskScheduler.Default);
return tcs.Task;
}
<Extension()>
Public Function AsApm(Of T)(task As Task(Of T),
callback As AsyncCallback,
state As Object) As IAsyncResult
If task Is Nothing Then
Throw New ArgumentNullException("task")
End If
Dim tcs As New TaskCompletionSource(Of T)(state)
task.ContinueWith(Sub(antecedent)
If antecedent.IsFaulted Then
tcs.TrySetException(antecedent.Exception.InnerExceptions)
ElseIf antecedent.IsCanceled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(antecedent.Result)
End If
If callback IsNot Nothing Then
callback(tcs.Task)
End If
End Sub, TaskScheduler.Default)
Return tcs.Task
End Function
À présent, imaginez une situation dans laquelle vous avez l’implémentation TAP suivante :
public static Task<String> DownloadStringAsync(Uri url)
Public Shared Function DownloadStringAsync(url As Uri) As Task(Of String)
et que vous souhaitez fournir cette implémentation APM :
public IAsyncResult BeginDownloadString(Uri url,
AsyncCallback callback,
object state)
Public Function BeginDownloadString(url As Uri,
callback As AsyncCallback,
state As Object) As IAsyncResult
public string EndDownloadString(IAsyncResult asyncResult)
Public Function EndDownloadString(asyncResult As IAsyncResult) As String
L’exemple suivant montre une migration vers APM :
public IAsyncResult BeginDownloadString(Uri url,
AsyncCallback callback,
object state)
{
return DownloadStringAsync(url).AsApm(callback, state);
}
public string EndDownloadString(IAsyncResult asyncResult)
{
return ((Task<string>)asyncResult).Result;
}
Public Function BeginDownloadString(url As Uri,
callback As AsyncCallback,
state As Object) As IAsyncResult
Return DownloadStringAsync(url).AsApm(callback, state)
End Function
Public Function EndDownloadString(asyncResult As IAsyncResult) As String
Return CType(asyncResult, Task(Of String)).Result
End Function
Tâches et EAP
Envelopper une implémentation Event-based Asynchronous Pattern (EAP) est plus complexe qu’envelopper un modèle APM, car le modèle EAP est plus variable et moins structuré qu’un modèle APM. Par exemple, le code suivant enveloppe la méthode DownloadStringAsync
. DownloadStringAsync
accepte un URI, déclenche l’événement DownloadProgressChanged
lors du téléchargement afin de générer plusieurs statistiques sur la progression et déclenche l’événement DownloadStringCompleted
lorsque l’événement précédent est terminé. Le résultat final est une chaîne qui détient le contenu de la page au niveau de l’URI spécifié.
public static Task<string> DownloadStringAsync(Uri url)
{
var tcs = new TaskCompletionSource<string>();
var wc = new WebClient();
wc.DownloadStringCompleted += (s,e) =>
{
if (e.Error != null)
tcs.TrySetException(e.Error);
else if (e.Cancelled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(e.Result);
};
wc.DownloadStringAsync(url);
return tcs.Task;
}
Public Shared Function DownloadStringAsync(url As Uri) As Task(Of String)
Dim tcs As New TaskCompletionSource(Of String)()
Dim wc As New WebClient()
AddHandler wc.DownloadStringCompleted, Sub(s, e)
If e.Error IsNot Nothing Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
End Sub
wc.DownloadStringAsync(url)
Return tcs.Task
End Function
Tâches et handles d’attente
handles d’attente vers TAP
Bien que les handles d’attente n’implémentent pas un modèle asynchrone, les développeurs expérimentés peuvent utiliser la classe WaitHandle et la méthode ThreadPool.RegisterWaitForSingleObject pour recevoir des notifications asynchrones lorsqu’un handle d’attente est défini. Vous pouvez envelopper la méthode RegisterWaitForSingleObject pour autoriser une alternative basée sur les tâches pour toute attente synchrone sur un handle d’attente :
public static Task WaitOneAsync(this WaitHandle waitHandle)
{
if (waitHandle == null)
throw new ArgumentNullException("waitHandle");
var tcs = new TaskCompletionSource<bool>();
var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle,
delegate { tcs.TrySetResult(true); }, null, -1, true);
var t = tcs.Task;
t.ContinueWith( (antecedent) => rwh.Unregister(null));
return t;
}
<Extension()>
Public Function WaitOneAsync(waitHandle As WaitHandle) As Task
If waitHandle Is Nothing Then
Throw New ArgumentNullException("waitHandle")
End If
Dim tcs As New TaskCompletionSource(Of Boolean)()
Dim rwh As RegisteredWaitHandle = ThreadPool.RegisterWaitForSingleObject(waitHandle,
Sub(state, timedOut)
tcs.TrySetResult(True)
End Sub, Nothing, -1, True)
Dim t = tcs.Task
t.ContinueWith(Sub(antecedent)
rwh.Unregister(Nothing)
End Sub)
Return t
End Function
Cette méthode vous permet d’utiliser les implémentations WaitHandle existantes dans les méthodes asynchrones. Par exemple, si vous souhaitez limiter le nombre d’opérations asynchrones en cours d’exécution à un moment donné, vous pouvez utiliser un sémaphore (un objet System.Threading.SemaphoreSlim). Vous pouvez limiter à N le nombre d’opérations qui s’exécutent simultanément en initialisant le compteur du sémaphore à N, en attendant le sémaphore chaque fois que vous souhaitez effectuer une opération et en désactivant le sémaphore lorsque vous avez terminé une opération :
static int N = 3;
static SemaphoreSlim m_throttle = new SemaphoreSlim(N, N);
static async Task DoOperation()
{
await m_throttle.WaitAsync();
// do work
m_throttle.Release();
}
Shared N As Integer = 3
Shared m_throttle As New SemaphoreSlim(N, N)
Shared Async Function DoOperation() As Task
Await m_throttle.WaitAsync()
' Do work.
m_throttle.Release()
End Function
Vous pouvez également créer un sémaphore asynchrone qui ne repose pas sur les handles d’attente, mais sur les tâches. Pour ce faire, vous pouvez utiliser des techniques telles que celles abordées dans Consuming the Task-based Asynchronous Pattern pour la création de structures de données sur Task.
TAP vers handles d’attente
Comme mentionné précédemment, la classe Task implémente IAsyncResult, et cette implémentation expose une propriété IAsyncResult.AsyncWaitHandle qui renvoie un handle d’attente qui sera défini une fois la Task terminée. Vous pouvez obtenir un WaitHandle pour une Task comme suit :
WaitHandle wh = ((IAsyncResult)task).AsyncWaitHandle;
Dim wh As WaitHandle = CType(task, IAsyncResult).AsyncWaitHandle