Remarque
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.
Les méthodes lambda asynchrones et anonymes sont des fonctionnalités puissantes qui vous permettent de créer des délégués représentant des opérations asynchrones. Utilisez-les avec des API conçues pour les délégués asynchrones. Cet article présente d’abord les modèles appropriés, puis explique ce qui se passe mal lorsque vous passez des lambda asynchrones aux API qui attendent des délégués synchrones.
Lambdas asynchrones associées aux délégués Action
Créez une surcharge qui accepte Func<Task> et attendez le résultat :
public static class TimingHelperFixed
{
public static double Time(Action action, int iterations = 10)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
action();
return sw.Elapsed.TotalSeconds / iterations;
}
public static async Task<double> TimeAsync(Func<Task> func, int iterations = 10)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
await func();
return sw.Elapsed.TotalSeconds / iterations;
}
}
public static class ActionFixDemo
{
public static async Task Run()
{
// Now the async lambda maps to Func<Task>, and
// the timer awaits each iteration to complete.
double seconds = await TimingHelperFixed.TimeAsync(async () =>
{
await Task.Delay(100);
}, iterations: 3);
Console.WriteLine($"Async (fixed): {seconds:F4}s per iteration");
}
}
Public Module TimingHelperFixed
Public Function Time(action As Action, Optional iterations As Integer = 10) As Double
Dim sw = Stopwatch.StartNew()
For i As Integer = 0 To iterations - 1
action()
Next
Return sw.Elapsed.TotalSeconds / iterations
End Function
Public Async Function Time(func As Func(Of Task), Optional iterations As Integer = 10) As Task(Of Double)
Dim sw = Stopwatch.StartNew()
For i As Integer = 0 To iterations - 1
Await func()
Next
Return sw.Elapsed.TotalSeconds / iterations
End Function
End Module
Public Module ActionFixDemo
Public Async Function Run() As Task
' Now the async lambda maps to Func(Of Task), and
' the timer waits for each iteration to complete.
Dim seconds As Double = Await TimingHelperFixed.Time(
Async Function()
Await Task.Delay(100)
End Function, iterations:=3)
Console.WriteLine($"Async (fixed): {seconds:F4}s per iteration")
End Function
End Module
Chaque fois que vous passez une lambda asynchrone à une méthode, vérifiez le type de délégué du paramètre. Si le paramètre est Action, Action<T>, ou tout autre délégué retournant void, passez à un délégué retournant une tâche pour les opérations asynchrones.
Une lambda asynchrone peut correspondre à un type de délégué retournant un void comme Action, en plus de Func<Task>. Lorsque le paramètre cible est un Action, le compilateur mappe l’lambda asynchrone à une méthode async void. L’appelant n’a aucun moyen de suivre l’achèvement.
Considérez un assistant de synchronisation qui accepte un Action:
public static class TimingHelper
{
public static double Time(Action action, int iterations = 10)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
action();
return sw.Elapsed.TotalSeconds / iterations;
}
}
public static class ActionPitfallDemo
{
public static void Run()
{
// Synchronous lambda — timing is accurate.
double syncSeconds = TimingHelper.Time(() =>
{
Thread.Sleep(100);
}, iterations: 3);
Console.WriteLine($"Sync: {syncSeconds:F4}s per iteration");
// Async lambda — becomes async void, returns immediately.
double asyncSeconds = TimingHelper.Time(async () =>
{
await Task.Delay(100);
}, iterations: 3);
Console.WriteLine($"Async (buggy): {asyncSeconds:F4}s per iteration");
}
}
Public Module TimingHelper
Public Function Time(action As Action, Optional iterations As Integer = 10) As Double
Dim sw = Stopwatch.StartNew()
For i As Integer = 0 To iterations - 1
action()
Next
Return sw.Elapsed.TotalSeconds / iterations
End Function
End Module
Public Module ActionPitfallDemo
Public Sub Run()
' Synchronous lambda — timing is accurate.
Dim syncSeconds As Double = TimingHelper.Time(
Sub() Thread.Sleep(100), iterations:=3)
Console.WriteLine($"Sync: {syncSeconds:F4}s per iteration")
' Async lambda — becomes Async Sub, returns immediately.
Dim asyncSeconds As Double = TimingHelper.Time(
Async Sub() Await Task.Delay(100), iterations:=3)
Console.WriteLine($"Async (buggy): {asyncSeconds:F4}s per iteration")
End Sub
End Module
Lorsque vous passez une lambda synchrone, le temps mesuré est précis. Avec une lambda asynchrone, le Action délégué retourne dès que le premier await produit, de sorte que le minuteur capture uniquement la partie synchrone au lieu de l’opération complète.
Parallel.ForEach avec des lambdas asynchrones
Dans .NET 6 et versions ultérieures, utilisez ForEachAsync, qui accepte un Func<TSource, CancellationToken, ValueTask> :
public static class ParallelForEachFixDemo
{
public static async Task RunAsync()
{
var sw = Stopwatch.StartNew();
await Parallel.ForEachAsync(
Enumerable.Range(0, 10),
new ParallelOptions { MaxDegreeOfParallelism = 4 },
async (i, ct) =>
{
await Task.Delay(200, ct);
});
Console.WriteLine($"Parallel.ForEachAsync (fixed): {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module ParallelForEachFixDemo
Private Function ProcessItemAsync(i As Integer, ct As CancellationToken) As ValueTask
Return New ValueTask(Task.Delay(200, ct))
End Function
Public Async Function RunAsync() As Task
Dim sw = Stopwatch.StartNew()
Await Parallel.ForEachAsync(
Enumerable.Range(0, 10),
New ParallelOptions With {.MaxDegreeOfParallelism = 4},
AddressOf ProcessItemAsync)
Console.WriteLine($"Parallel.ForEachAsync (fixed): {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
Vous pouvez également projeter les éléments en tâches et utiliser WhenAll:
public static class WhenAllAlternativeDemo
{
public static async Task RunAsync()
{
var sw = Stopwatch.StartNew();
var tasks = Enumerable.Range(0, 10)
.Select(async i =>
{
await Task.Delay(200);
});
await Task.WhenAll(tasks);
Console.WriteLine($"Task.WhenAll: {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module WhenAllAlternativeDemo
Public Async Function RunAsync() As Task
Dim sw = Stopwatch.StartNew()
Dim tasks = Enumerable.Range(0, 10).
Select(Async Function(i)
Await Task.Delay(200)
End Function)
Await Task.WhenAll(tasks)
Console.WriteLine($"Task.WhenAll: {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
ForEach accepte un paramètre 'body' Action<T>. Le passage d’une expression lambda asynchrone crée un délégué async void.
Parallel.ForEach retourne dès que chaque délégué atteint son premier rendement await:
public static class ParallelForEachBugDemo
{
public static void Run()
{
var sw = Stopwatch.StartNew();
Parallel.ForEach(Enumerable.Range(0, 10), async i =>
{
await Task.Delay(200);
});
// Completes almost immediately — the async lambdas are fire-and-forget.
Console.WriteLine($"Parallel.ForEach (buggy): {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module ParallelForEachBugDemo
Public Sub Run()
Dim sw = Stopwatch.StartNew()
Parallel.ForEach(Enumerable.Range(0, 10),
Async Sub(i As Integer)
Await Task.Delay(200)
End Sub)
' Completes almost immediately — the async lambdas are fire-and-forget.
Console.WriteLine($"Parallel.ForEach (buggy): {sw.Elapsed.TotalSeconds:F2}s")
End Sub
End Module
La boucle se termine en quelques millisecondes au lieu de la durée attendue, car les fonctions lambda asynchrones se transforment en opérations sans retour.
Task.Factory.StartNew avec des lambdas asynchrones
Run démasque automatiquement les lambdas asynchrones. Il accepte les surcharges Func<Task> et Func<Task<TResult>>, et retourne la tâche interne.
public static class StartNewFix1Demo
{
public static async Task RunAsync()
{
var sw = Stopwatch.StartNew();
await Task.Run(async () =>
{
await Task.Delay(1000);
});
Console.WriteLine($"Task.Run (fixed): {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module StartNewFix1Demo
Public Async Function RunAsync() As Task
Dim sw = Stopwatch.StartNew()
Await Task.Run(Async Function()
Await Task.Delay(1000)
End Function)
Console.WriteLine($"Task.Run (fixed): {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
Si vous avez besoin StartNewd’options spécifiques (par exemple LongRunning), appelez Unwrap le résultat :
public static class StartNewFix2Demo
{
public static async Task RunAsync()
{
var sw = Stopwatch.StartNew();
await Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
}).Unwrap();
Console.WriteLine($"StartNew + Unwrap (fixed): {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module StartNewFix2Demo
Public Async Function RunAsync() As Task
Dim sw = Stopwatch.StartNew()
Await Task.Factory.StartNew(Async Function()
Await Task.Delay(1000)
End Function).Unwrap()
Console.WriteLine($"StartNew + Unwrap (fixed): {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
Lorsque vous passez une lambda asynchrone à StartNew, le type de retour est Task<Task> (ou Task<Task<TResult>>). La tâche externe représente uniquement la partie synchrone du délégué ; elle se termine au premier point de suspension await. La tâche interne représente l’opération asynchrone complète :
public static class StartNewBugDemo
{
public static async Task RunAsync()
{
var sw = Stopwatch.StartNew();
// t is Task<Task> — the outer task completes at the first yielding await.
Task<Task> t = Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
});
await t; // Awaits only the outer task.
Console.WriteLine($"StartNew (buggy): {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module StartNewBugDemo
Public Async Function RunAsync() As Task
Dim sw = Stopwatch.StartNew()
' t is Task(Of Task) — the outer task completes at the first yielding Await.
Dim t As Task(Of Task) = Task.Factory.StartNew(Async Function()
Await Task.Delay(1000)
End Function)
Await t ' Awaits only the outer task.
Console.WriteLine($"StartNew (buggy): {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
Si vous traitez la tâche externe comme l’ensemble de l’opération, vous observerez l’achèvement avant que le travail asynchrone se termine réellement.
Résumé
Lorsque vous passez une lambda asynchrone à n’importe quelle méthode, vérifiez le type de délégué du paramètre cible :
| Type de délégué | Comportement asynchrone | Risque |
|---|---|---|
Func<Task>, Func<Task<T>> |
L’appelant reçoit une tâche qui représente l’achèvement | Sûr |
Action, Action<T> |
Devient async void : l’appelant ne peut pas observer l’achèvement | Élevé |
Func<TResult> où TResult est Task |
Renvoie Task<Task> ; la tâche externe ne représente pas le travail complet |
Moyenne |