1. Async / Await 란?
비동기 프로그래밍을 간편하게 만들어주는 문법적 도우미.
컴파일러가 자동으로 생성하는 "상태 기계"에 핵심이 숨어 있다.
2. Async / Await
2.1 상태 기계
메서드의 실행 상태(어느 부분까지 실행되었는지, 지역 변수 값 등) 을 저장하는 객체
해당 객체는 일반적으로 힙에 할당되며, 메서드가 중단되었다가 재개될 때 필요한 정보를 가지고 있다.
1. aysnc 메서드가 컴파일될 때, 컴파일러는 해당 메서드를 IAsyncStateMachine 인터페이스 구현
- 이 내부 클래스는 메서드의 지역 변수, 현재 상태, Task의 결과를 전달할 빌더 등을 필드로 갖는다.
2. IAsyncStateMachine 인터페이스는 MoveNext 메서드를 요구한다.
MoveNext의 경우, switch 문이나 비슷한 제어 구조가 들어 있어, 현대 상태에 따라 어느 코드 블럭을
실행해야 하는지를 결정.
await 중단 시 : 현재 상태를 저장하고, 비동기 작업(Task)의 완료를 기다리도록 설정 한 후 MoveNext의 실행 중단.
await 재개 시 : 비동기 작업이 완료되면, awiat에 등록된 콜백을 통해 MoveNext가 다시 호출.
저장된 값을 이용하여 "일시 중단" 되었던 지점부터 실행을 재
아래의 경우, 해당 aysnc / await 사용 함수를 실제로 컴파일러가 변경한 내용이다.
public async Task<int> ComputeAsync()
{
int a = 10;
int b = await GetNumberAsync(); // 비동기 작업에서 await
return a + b;
}
private struct ComputeAsyncStateMachine : IAsyncStateMachine
{
public int state; // 실행 상태를 나타내는 변수 (-1: 완료, 0: 시작, 1: await 이후 등)
public AsyncTaskMethodBuilder<int> builder;
// 메서드의 지역 변수는 필드로 승격됨
private int a;
private TaskAwaiter<int> awaiter;
public void MoveNext()
{
int result;
try
{
if (state == 0)
{
// 초기 상태: a를 초기화
a = 10;
// GetNumberAsync() 호출 및 awaiter에 대입
awaiter = GetNumberAsync().GetAwaiter();
if (!awaiter.IsCompleted)
{
// await 비동기 작업이 아직 완료되지 않음: 상태를 저장하고 중단
state = 1;
builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
if (state == 1)
{
// await 비동기 작업이 완료되어 재개됨
int b = awaiter.GetResult();
result = a + b;
}
else
{
// 초기 상태에서 바로 완료된 경우
int b = awaiter.GetResult();
result = a + b;
}
}
catch (Exception ex)
{
builder.SetException(ex);
return;
}
builder.SetResult(result);
}
public void SetStateMachine(IAsyncStateMachine stateMachine) { /* 생략 */ }
}
Awaiter란?
aysnc / await 매커니즘에서 비동기 작업의 진행 상태를 확인하고, 완료 시 후속 작업을 실행하기 위한 역할을 하는 객체
bool IsCompleted { get; }
: 비동기 작업이 완료되었는지 여부.
void OnCompleted(Action continuation)
비동기 작업이 완료되었을 때 호출할 콜백을 등록
T GetResult()
: 비동기 작업이 완료된 후 결과를 가져오거나, 예외 발생시 예외를 던진다.
2.2 예시 1
using System;
using System.Threading.Tasks;
namespace AsyncTest
{
class Program
{
public static void Main(string[] args)
{
System.Console.WriteLine("Do Something Before TaskTest");
var result = TaskTest();
System.Console.WriteLine($"{result.Status} data");
System.Console.WriteLine("Do Something after TaskTest");
Console.ReadLine();
}
private static async Task<long> TaskTest()
{
System.Console.WriteLine("TaskTest start");
var a = await GetNumber();
System.Console.WriteLine("TaskTest Done");
return a;
}
private static Task<long> GetNumber()
{
System.Console.WriteLine("GetNumber start");
return Task.Run(() =>
{
Console.WriteLine($"Run Run Run"); // 출력하면 최적화 방지 가능
long a = 0;
for (int i = 0; i < 10_000_000; ++i)
{
++a;
}
Console.WriteLine($"Final a: {a}"); // 출력하면 최적화 방지 가능
return a;
}); ;
}
}
}
결과값
Do Something Before TaskTest
TaskTest start
GetNumber start
WaitingForActivation data
Do Something after TaskTest
Run Run Run
Final a: 10000000
TaskTest Done
2.3 예시 2
만약 async의 결과값을 확인되면 어떻게 되는가?
using System;
using System.Threading.Tasks;
namespace AsyncTest
{
class Program
{
public static void Main(string[] args)
{
System.Console.WriteLine("Do Something Before TaskTest");
var result = TaskTest();
// result.Result 추가
if(result.Result > 0)
{
System.Console.WriteLine($"{result.Result} data");
}
System.Console.WriteLine($"{result.Status} data");
System.Console.WriteLine("Do Something after TaskTest");
Console.ReadLine();
}
private static async Task<long> TaskTest()
{
System.Console.WriteLine("TaskTest start");
var a = await GetNumber();
System.Console.WriteLine("TaskTest Done");
return a;
}
private static Task<long> GetNumber()
{
System.Console.WriteLine("GetNumber start");
return Task.Run(() =>
{
Console.WriteLine($"Run Run Run"); // 출력하면 최적화 방지 가능
long a = 0;
for (int i = 0; i < 10_000_000; ++i)
{
++a;
}
Console.WriteLine($"Final a: {a}"); // 출력하면 최적화 방지 가능
return a;
}); ;
}
}
}
Do Something Before TaskTest
TaskTest start
GetNumber start
Run Run Run
Final a: 10000000
TaskTest Done
10000000 data
RanToCompletion data
Do Something after TaskTest
2.4 이해를 위한 추가 예시 ( MSDN )
msdn 에서 제공하는 좋은 예시 ( 아침 식사를 준비하는 예시 )
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("eggs are ready");
Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacons = await baconTask;
Console.WriteLine("bacon is ready");
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
Console.ReadLine();
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
/*
해당 경우에서만, async를 정상적으로 사용하는 것으로 보인다.
물론 해당 await가 선언된 순서대로 함수가 끝난지를 판단해서 처리가 진행된다.
함수를 호출 할때, 이미 함수는 실행이 된다!
await로 함수가 끝났는지를 판단만 한다.
*/
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("eggs are ready");
Bacon bacons = await baconTask;
Console.WriteLine("bacon is ready");
Console.WriteLine("Breakfast is ready!");
Console.ReadLine();
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
2.3 WhenAll / WhenAny 사용 예시
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Console.WriteLine("Breakfast is ready!");
Console.ReadLine();
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
4. 참고 자료
자세한 예시가 있는 MSDN 정식 사이트
C#의 비동기 프로그래밍 - C#
async, await 및 Task를 사용하여 비동기 프로그래밍을 지원하는 C# 언어에 대해 간략히 설명합니다.
learn.microsoft.com
'[ 공 부 ] > [ C# ]' 카테고리의 다른 글
05장. Switch (0) | 2025.02.23 |
---|---|
C# 프로그래밍 간단 정리 (1) | 2025.02.19 |
22장. 가비지 컬렉션 (0) | 2025.01.31 |
21장. 네트워크 프로그래밍 (0) | 2025.01.31 |
20장. WinForm (0) | 2025.01.30 |