Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Асинхронные лямбда-методы и анонимные методы — это мощные функции, позволяющие создавать делегаты, представляющие асинхронные операции. Используйте их с API, предназначенными для асинхронных делегатов. В этой статье сначала показаны правильные шаблоны, а затем объясняется, что происходит неправильно при передаче асинхронных лямбда-выражений в API, которые ожидают синхронных делегатов.
Асинхронные лямбда-выражения, Action назначенные делегатам
Создайте перегрузку, принимающую Func<Task> и дождитесь результата:
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
Каждый раз, когда вы передаете асинхронное лямбда-выражение в метод, проверьте тип делегата параметра. Если параметр имеет Actionзначение , Action<T>или любой другой делегат, возвращающий пустоту, переключитесь на делегат, возвращающий задачу для асинхронных операций.
Асинхронное лямбда-выражение может соответствовать типу делегата, который возвращает void, как, например, Action, в дополнение к Func<Task>. Если целевой параметр является Action, компилятор сопоставляет асинхронную лямбду с асинхронным методом void. Вызывающая сторона не может отслеживать завершение.
Рассмотрим вспомогательный модуль времени, принимающий: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
При передаче синхронной лямбда-функции, измеренное время точно. С асинхронной лямбда-делегатом Action возвращается сразу после первого вызова await, поэтому таймер захватывает только синхронную часть вместо полной операции.
Parallel.ForEach с асинхронными лямбдами
В .NET 6 и более поздних версий используйте ForEachAsync, который принимает 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
В качестве альтернативы можно преобразовать элементы в задачи и использовать 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 принимает Action<T> в качестве параметра body. Передача асинхронного лямбда-выражения создает делегат типа void.
Parallel.ForEachвозвращается, как только каждый делегат достигает своей первой точки возврата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
Цикл завершается в миллисекундах вместо ожидаемой длительности, так как асинхронные лямбда-операции становятся операциями fire-and-forget.
Task.Factory.StartNew с асинхронными лямбдами
Run автоматически распаковывает асинхронные лямбда-выражения. Он принимает перегрузки Func<Task> и Func<Task<TResult>> и возвращает внутреннюю задачу.
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
Если вам нужны параметры, специфичные для StartNew (такие как LongRunning), вызовите Unwrap на результате:
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
При передаче асинхронной лямбды в StartNew тип возвращаемого значения - это Task<Task> (или Task<Task<TResult>>). Внешняя задача представляет только синхронную часть делегата—она завершается при первом переключении await. Внутренняя задача представляет полную асинхронную операцию:
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
Если вы считаете внешнюю задачу всей операцией, то увидите её завершение до того, как асинхронная работа фактически завершится.
Сводка
При передаче асинхронной лямбды любому методу проверьте тип делегата целевого параметра.
| Тип делегата | Асинхронное поведение | Риск |
|---|---|---|
Func<Task>, Func<Task<T>> |
Вызывающий получает задачу, представляющую завершение | Безопасно |
Action, Action<T> |
Становится асинхронной пустотой— вызывающий не может наблюдать за завершением | Высокий |
Func<TResult> где TResult это Task |
Возвращает Task<Task>—внешняя задача не представляет полную работу |
Средняя |