비동기 프로그래밍은 시간 소모적인 작업이 프로그램의 흐름을 방해하지 않도록 하여, 더 빠르고 반응성이 좋은 애플리케이션을 만드는 데 필수적인 기법입니다.
1. 비동기의 필요성과 개념
비동기 프로그래밍이란?
비동기 프로그래밍은 시간이 오래 걸리는 작업(예: 파일 입출력, 네트워크 요청, 데이터베이스 쿼리 등)을 별도로 처리하고, 나머지 작업은 계속 진행하도록 만드는 프로그래밍 방식입니다. 주요 목표는 애플리케이션의 반응성을 유지하고, 리소스 낭비를 줄이는 것입니다.
비동기의 필요성
- UI 애플리케이션의 반응성 유지
예를 들어, 버튼 클릭 시 긴 작업이 진행되는 동안 UI가 멈추는 대신, 사용자는 여전히 애플리케이션과 상호작용할 수 있어야 합니다. - 효율적인 리소스 사용
CPU가 다른 작업을 처리할 수 있도록 대기 시간이 많은 작업(예: 네트워크 요청)은 비동기로 처리해야 합니다. - I/O 작업 최적화
파일 읽기/쓰기나 데이터베이스와 같은 작업은 비동기 방식으로 처리하는 것이 효율적입니다.
동기와 비동기의 비교
특징 | 동기 | 비동기 |
실행 방식 | 작업이 끝날 때까지 대기 | 작업을 시작한 뒤 다른 작업을 진행 |
UI 반응성 | 작업 중 UI가 멈춤 | UI가 멈추지 않음 |
리소스 사용 | 작업 동안 차단 | 효율적으로 리소스 활용 |
2. async와 await 키워드
C#에서는 async와 await 키워드를 사용하여 비동기 작업을 간단히 구현할 수 있습니다.
async 키워드
- 메서드가 비동기로 실행된다는 것을 나타냅니다.
- 반환 타입은 보통 Task 또는 Task<T>입니다. 반환값이 없으면 Task를, 반환값이 있으면 Task<T>를 사용합니다.
await 키워드
- 비동기 작업이 완료될 때까지 기다리며, 작업이 완료되면 실행 흐름을 이어갑니다.
- await는 async 메서드 안에서만 사용할 수 있습니다.
비동기 메서드 예제
using System;
using System.Threading.Tasks;
class Program
{
// 비동기 메서드 정의
static async Task LongRunningTask()
{
Console.WriteLine("작업 시작");
await Task.Delay(3000); // 3초 대기 (비동기적으로 대기)
Console.WriteLine("작업 완료");
}
static async Task Main(string[] args)
{
Console.WriteLine("Main 시작");
// 비동기 메서드 호출
await LongRunningTask();
Console.WriteLine("Main 종료");
}
}
💡 Task.Delay(3000)은 3초 동안 비동기적으로 대기하는 작업입니다. 이 동안 다른 작업이 수행될 수 있습니다.
3. Task와 Task<T> 클래스의 활용
Task와 Task<T>는 비동기 작업을 나타내는 클래스입니다.
Task 클래스
Task는 반환값이 없는 비동기 작업을 나타냅니다.
static async Task PrintMessageAsync()
{
await Task.Delay(2000); // 2초 대기
Console.WriteLine("Hello, Async!");
}
static async Task Main()
{
await PrintMessageAsync();
}
Task<T> 클래스
Task<T>는 반환값이 있는 비동기 작업을 나타냅니다.
static async Task<int> GetNumberAsync()
{
await Task.Delay(1000); // 1초 대기
return 42; // 값 반환
}
static async Task Main()
{
int result = await GetNumberAsync();
Console.WriteLine($"결과: {result}");
}
Task를 사용한 병렬 작업 처리
Task.WhenAll을 사용하면 여러 비동기 작업을 병렬로 실행하고, 모든 작업이 완료될 때까지 기다릴 수 있습니다.
static async Task<int> Task1()
{
await Task.Delay(2000);
Console.WriteLine("Task1 완료");
return 1;
}
static async Task<int> Task2()
{
await Task.Delay(1000);
Console.WriteLine("Task2 완료");
return 2;
}
static async Task Main()
{
// 여러 Task 병렬 실행
Task<int> t1 = Task1();
Task<int> t2 = Task2();
// 모든 작업 완료 대기
int[] results = await Task.WhenAll(t1, t2);
Console.WriteLine($"총합: {results[0] + results[1]}"); // 결과: 3
}
💡 Task.WhenAll을 사용하면 병렬로 작업을 처리하여 성능을 최적화할 수 있습니다.
비동기 프로그래밍에서의 주의점
- 데드락 방지
- UI 애플리케이션에서는 Task.Wait()나 Task.Result를 사용하면 데드락이 발생할 수 있습니다. 항상 await를 사용하세요.
// 잘못된 예 static void Main() { Task<int> task = GetNumberAsync(); Console.WriteLine(task.Result); // 데드락 가능 }
- UI 애플리케이션에서는 Task.Wait()나 Task.Result를 사용하면 데드락이 발생할 수 있습니다. 항상 await를 사용하세요.
- Exception Handling
- 비동기 메서드에서 발생한 예외는 try-catch를 통해 처리해야 합니다.
static async Task<int> DivideAsync(int numerator, int denominator) { if (denominator == 0) { throw new DivideByZeroException("0으로 나눌 수 없습니다."); } return numerator / denominator; } static async Task Main() { try { int result = await DivideAsync(10, 0); Console.WriteLine($"결과: {result}"); } catch (DivideByZeroException ex) { Console.WriteLine($"예외 발생: {ex.Message}"); } }
- 비동기 메서드에서 발생한 예외는 try-catch를 통해 처리해야 합니다.
- 캔슬레이션 토큰
- 작업을 취소하려면 CancellationToken을 사용할 수 있습니다.
static async Task RunWithCancellation(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine($"작업 {i} 진행 중...");
await Task.Delay(500, token); // 취소 가능
}
}
static async Task Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
Task task = RunWithCancellation(cts.Token);
await Task.Delay(1000); // 1초 대기 후 취소
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("작업이 취소되었습니다.");
}
}
728x90
반응형