반응형
async/await 이란?
비동기 프로그래밍을 쉽게 구현할 수 있게 해주는 기능
유니티에서는 코루틴 대신 사용할 수 있는 좋은 대안이다
async의 핵심 정의
- 메서드나 람다 표현식을 비동기 메서드로 지정하는 키워드
- async가 붙은 메서드는 내부에서 await를 사용할 수 있음
- 컴파일러에게 "이 메서드는 비동기적으로 실행될 수 있다"라고 알려주는 역할을 함
- 반환 타입으로 Task, Task<T>, void를 사용할 수 있음
예시코드
public async Task DoSomethingAsync()
{
// 비동기 코드 작성 가능
}
await의 핵심 정의
- 비동기 작업이 완료될 때까지 현재 메서드의 실행을 일시 중단하는 키워드
- await는 반드시 async 메서드 내부에서만 사용할 수 있음
- 작업이 완료되면 await 이후의 코드가 실행됨
- 비동기 작업의 결과를 동기적인 코드처럼 받을 수 있게 해줌
예시코드
public async Task<string> GetDataAsync()
{
var result = await SomeAsyncOperation(); // 이 작업이 끝날 때까지 대기
return result; // 작업이 완료되면 실행
}
Task란?
비동기적으로 실행될 작업을 나타내는 객체
쉽게 말하자면
지금 당장은 결과를 모르지만, 미래의 어떤 시점에 완료될 작업을 표현한다
Task는 작업의 상태 (실행 중, 완료, 실패 등)를 추적할 수 있다
간단한 예시
// string을 반환하는 비동기 작업
public async Task<string> GetMessageAsync()
{
await Task.Delay(1000); // 1초 대기
return "안녕하세요!";
}
// 아무것도 반환하지 않는 비동기 작업
public async Task ShowMessageAsync()
{
await Task.Delay(1000); // 1초 대기
Debug.Log("완료!");
}
제어흐름
Task가 await를 만나면 해당 메서드는 작업이 완료될 때까지 대기하지만,
스레드를 차단하지 않고 제어권을 호출자에게 반환한다.
이해를 돕기 위해 비교를 해보았다
일반적인 동기 처리 ( 차단 )
void DoSomething()
{
Thread.Sleep(5000); // 5초 동안 현재 스레드가 멈춤
Debug.Log("완료"); // 5초 후에 실행
}
// 이 메서드를 호출하면 프로그램이 5초 동안 완전히 멈춤
비동기 처리 ( 비차단 )
async Task DoSomethingAsync()
{
await Task.Delay(5000); // 5초를 기다리지만, 프로그램은 계속 실행됨
Debug.Log("완료"); // 5초 후에 실행
}
// 이 메서드를 호출해도 프로그램은 계속 실행됨
조금 더 자세한 실행순서
public async Task ProcessDataAsync()
{
Debug.Log("1. 작업 시작");
await Task.Delay(1000); // 여기서 Task가 await를 만남
Debug.Log("3. 작업 재개"); // 1초 후에 실행됨
}
public async Task MainMethodAsync()
{
Debug.Log("시작");
var task = ProcessDataAsync(); // ProcessDataAsync가 await를 만나면 여기로 제어가 돌아옴
Debug.Log("2. 다른 작업 실행"); // ProcessDataAsync가 대기하는 동안 실행됨
await task; // ProcessDataAsync의 완료를 기다림
Debug.Log("끝");
}
실행결과
시작
1. 작업 시작
2. 다른 작업 실행
3. 작업 재개
끝
즉, 스레드를 차단하지 않는다는 것은
시간이 오래 걸리는 작업을 실행하면서도 프로그램의 다른 부분들은
정상적으로 계속 동작할 수 있다는 의미이다.
실제 사용 예시
public async void StartGame()
{
Debug.Log("게임 시작!");
// 리소스를 로딩하는 동안에도 플레이어는 로딩 화면을 볼 수 있고
// 프로그램은 계속 응답 가능한 상태를 유지합니다
await LoadResourcesAsync();
Debug.Log("리소스 로딩 완료!");
}
효율적인 사용방법 및 주의사항
1. async void 사용을 피해야 하는 패턴
async void는 예외 처리가 불가능해서 애플리케이션이 갑자기 종료된다는 등 위험하다
// 나쁜 예시
public async void SaveData() // void라서 에러 추적 불가
{
await WriteToFile(); // 여기서 에러나면?
}
// 좋은 예시
public async Task SaveData() // Task를 사용해서 에러 처리 가능
{
try
{
await WriteToFile();
}
catch (Exception e)
{
Debug.LogError("파일 저장 실패: " + e.Message);
// 에러 처리 가능
}
}
2. ConfigureAwait 패턴
UI 작업이 필요없는 경우 성능을 향상시킬 수 있다
// UI 업데이트가 필요한 경우
public async Task UpdatePlayerUI()
{
var data = await GetPlayerData(); // UI 스레드로 돌아옴
playerNameText.text = data.name; // UI 업데이트
}
// UI 업데이트가 필요없는 경우
public async Task ProcessGameData()
{
var data = await GetPlayerData().ConfigureAwait(false); // 아무 스레드나 사용
ProcessData(data); // UI 작업 없음
}
3. 병렬 처리 패턴
여러 독립적인 작업을 동시에 실행할 수 있다
// 느린 방법
public async Task LoadGameAsync()
{
await LoadMapAsync(); // 3초
await LoadCharacterAsync(); // 2초
await LoadItemsAsync(); // 1초
// 총 6초 걸림
}
// 빠른 방법
public async Task LoadGameAsync()
{
var mapTask = LoadMapAsync(); // 시작
var characterTask = LoadCharacterAsync(); // 시작
var itemsTask = LoadItemsAsync(); // 시작
await Task.WhenAll(mapTask, characterTask, itemsTask);
// 총 3초만 걸림 (가장 오래 걸리는 작업 기준)
}
4. 작업 취소 패턴
오래 걸리는 작업을 중간에 취소할 수 있게 해준다
public class GameManager : MonoBehaviour
{
private CancellationTokenSource _cts;
public async Task LoadLevelAsync()
{
_cts = new CancellationTokenSource();
try
{
await LoadResourcesAsync(_cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("로딩이 취소되었습니다");
}
}
public void CancelLoading()
{
_cts?.Cancel(); // 로딩 취소
}
}
5. 데드락 방지 패턴
동기와 비동기 코드를 잘못 섞으면 프로그램이 완전히 멈출 수 있다
// 데드락 발생 코드
void Start()
{
var task = LoadDataAsync();
task.Wait(); // 여기서 멈춤!
}
// 올바른 코드
async void Start()
{
await LoadDataAsync(); // 정상 작동
}
6.async 람다 패턴
이벤트 핸들러에서 비동기 작업을 처리할 때 유용하다
// UI 버튼에 비동기 이벤트 연결
saveButton.onClick.AddListener(async () =>
{
saveButton.interactable = false; // 버튼 비활성화
await SaveGameAsync(); // 저장 작업
saveButton.interactable = true; // 버튼 다시 활성화
});
데드락이란?
두 개 이상의 작업이 서로가 가진 리소스를 기다리면서 영원히 멈춰있는 상태
간단 예시
// 데드락 발생 상황
public class DeadlockExample : MonoBehaviour
{
private async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "데이터";
}
private void Start()
{
// 메인 스레드에서 실행
var task = GetDataAsync(); // 비동기 작업 시작
var result = task.Result; // 여기서 데드락!
}
}
위 코드에서 데드락이 발생하는 이유
- Start() 메서드에서 GetDataAsync()를 호출
- GetDataAsync() 안에서 await Task.Delay(1000)를 만남
- await를 만났으니 제어권이 Start() 메서드로 돌아옴
- Start() 메서드에서는 task.Result를 기다림
- 하지만 Result를 받으려면 GetDataAsync()가 완료되야함
- GetDataAsync()는 await 이후의 코드를 실행해야 하는데,
이를 위해서는 Start() 메서드의 스레드(메인 스레드)가 필요함 - 그러나 메인 스레드는 이미 Result를 기다리고 있는 상태임
- 데드락 상황 발생!
해결방법
private async void Start()
{
// await를 사용하여 데드락 방지
var result = await GetDataAsync();
}
이를 실행 흐름을 순서대로 나타내보면 아래와 같은 결과가 나온다
private async void Start()
{
Debug.Log("1. Start 시작!");
var result = await GetDataAsync(); // GetDataAsync로 이동
Debug.Log("4. 최종 result: " + result); // "데이터" 출력
}
private async Task<string> GetDataAsync()
{
Debug.Log("2. GetDataAsync 시작!");
await Task.Delay(1000); // 1초 대기
Debug.Log("3. 1초 지남, 데이터 반환!");
return "데이터";
}
실행결과
1. Start 시작!
2. GetDataAsync 시작!
(1초 대기)
3. 1초 지남, 데이터 반환!
4. 최종 result: 데이터
반응형
'유니티 > 문법' 카테고리의 다른 글
[Unity] 유니티 C#) async와 코루틴의 차이 (0) | 2025.01.22 |
---|---|
[Unity] 유니티 C#) Property ( 프로퍼티 ) (0) | 2025.01.20 |
[Unity] 유니티C#) Struct ( 구조체 ) (1) | 2025.01.17 |
[Unity] 유니티 C#) Casting ( 형변환 ) (0) | 2025.01.16 |
[Unity] 유니티 C#) try-catch문 ( 예외처리 ) (1) | 2025.01.15 |