비동기 람다 및 익명 메서드는 비동기 작업을 나타내는 대리자를 만들 수 있는 강력한 기능입니다. 비동기 대리자를 위해 설계된 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 반환 대리자인 경우 비동기 작업을 위해 task 반환 대리자로 전환합니다.
비동기 람다는 Func<Task> 외에도 void를 반환하는 Action 대리자 형식과 일치할 수 있습니다. 대상 매개 변수가 있으면 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>을/를 허용합니다. 비동기 람다를 전달하면 비동기 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
비동기 람다 함수가 실행 후 신경 쓰지 않는 작업이 되어, 루프는 예상된 시간 대신 밀리초 단위로 완료됩니다.
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> |
비동기 함수가 되어 호출자가 완료 여부를 알 수 없습니다. | 높음 |
Task인 TResult의 위치는 Func<TResult> |
반환 Task<Task>- 외부 작업이 전체 작업을 나타내지 않음 |
중간 |
참고하십시오
.NET