우선 동기 비동기를 복습해보자.
동기
어떤 작업을 실행하고 해당작업이 끝날 때 까지 기다린 후 다음 작업을 실행함.
비동기
어떤 작업을 실행하고 해당 작업이 끝남을 기다리지 않고 바로 다음 작업을 실행함.
async / await
- 비동기 작업을 동기작업 처럼 처리하는 기능을 제공함.
- Swift 5.5에서 탄생
- 비동기 코드를 동기인것마냥 작성할 수 있다
Completion Handler의 문제점
- 기존의 CompletionHandler는 에러일 때 Completion Handler 호출을 잊어버리는 문제
- gaurd let else 문에서 호출을 깜빡함.
- 동기적으로 수행해야할 비동기 함수가 많을 경우 depth가 깊어지는 문제
- 가독성이 떨어지는 문제
- 실패, 성공에 따라 분기가 복잡해지는 문제
이처럼 다양한 문제가 존재한다.
await / async를 사용하면 이처럼 복잡한 코드가 아래처럼 단순화 된다.
또한 모든 클로저와 Depth가 사라져 가독성이 좋아졌다.
사용방법
** await과 async는 짝꿍이다!!
** 마치 try와 throws 처럼!!
async
- asycn 키워드는 비동기로 처리되는 것을 의미한다.
- '함수명()' 오른쪽에 적는다.
- 에러를 던질 수 있는 경우 async throws 순서로 적는다.
- 프로토콜에서도 요구할 async로 요구할 수 있으며 구현부에선 async로 구현해야 한다.
await
- async 함수를 호출하기 위해선 await 키워드가 필요하다.
- 따라서 URLSession함수도 async 함수임을 알 수 있다.
await키워드로 마킹 된 곳은 potential suspension point(잠재적 일시 중단 지점)로 지정된다.
async로 선언한 함수가 완료될 때까지 일시 중지 되는 지점이다. 예를 들어, 네트워크로 요청한 데이터를 다 가져올 때까지 작업을 일시 중단 해야 할 것이다.
async로 데이터 요청을 하고 await 지점에서 대기하는 것이다.
** async 메서드의 사용
- async 메서드를 호출하려면 async 메서드 내에서 호출해야한다.
- 아니라면 **Task로 묶어서 호출해야 한다.
**Task 주의점
각각의 Task는 병렬로 실행된다.
예제 1) async / await
awaitTest는 "Task.sleep" 라는 비동기 코드를 우선 실행시키고 await한다.
만약 await을 사용하지 않고 동기적으로 실행시켰다면 sleep 1초를 기다리지 않고 즉시 "async함수 끝"을 리턴할 것이다.
async throws를 사용한 이유는 Task.sleep가 throws함수이기 때문이다.
이 await 함수를 실행시켜보자.
Task{
let string = try await awaitTest()
print(string)
print("Task 종료")
}
// 실행결과
// ...1초 뒤
// async함수 끝
// Task 종료
**예제 2) Task의 병렬 실행
이 코드의 실행 결과는 어떻게 될까?
난 먼저 간다.. -> 난 늦게 가나?.. -> 1초뒤... -> async 함수 끝 -> Task 종료
await블럭의 Suspend 상태
await블럭을 만나는 곳은 Potension suspend point(잠재적 일시 중단 상태)가 됩니다.
여기서 Suspen 상태라는 의미가 중요한데,
Suspend 상태라는 의미는 스레드의 blockd이 아닌 사용중인 스레드가 다른 동작을 할 수 있게 제어권을 놓아준다 라는 의미입니다.
쉽게 설명하기 위해 차근 차근 하나씩 봅시다.
Sync에서의 Thread 제어권을 먼저 봅시다.
syncA와 syncB는 모두 sync(동기)함수입니다.
syncA를 호출하는 경우 Thread 제어권은 아래와 같습니다.
syncB를 만나면 syncB에게 스레드 제어권을 넘깁니다. syncA는 그동안 작업을 할 수 없습니다.
그리고 syncB가 종료되면 제어권을 syncA로 넘깁니다. 이 후 syncA는 작업을 이어나갑니다.
async에서 Thread 제어권을 봅시다
언뜻 보기에는 sync와 다를게 없어보입니다. 하지만 분명히 다른게 존재합니다.
asyncA를 호출하면 위의 sync함수와 동일하게 스레드 제어권은 Task.sleep으로 넘어갑니다.
그리고 sleep가 종료되면 제어권을 다시 syncA로 돌려줍니다.
하지만 Task.sleep은 async함수 이기 때문에 중간에 suspend될 수 있습니다. 그런경우 syncA도 suspend 됩니다.
여기서 suspend된다는 것은 스레드의 제어권을 놓아준다(포기한다)라는 의미입니다.
이렇게 suspend된 스레드 제어권은 system에게 할당됩니다. 이 후 system은 스레드를 사용하여 다른 작업을 수행할 수 있는 상태가 됩니다.
그 후 시스템은 우선순위등을 판단해가며 여러 작업을 실행할 수 있습니다.
그리고 어느 시점에 중단된 async 함수를 재개하는것이 중요하다고 판단되면
해당 함수를 resume하게 되고 다시 스레드를 함수(Task.sleep)에 할당해 작업을 이어나갑니다.
이를 그림으로 그려보면
이런 상태가 됩니다.
다시 한 번 정리해보면
1. await함수를 만나면 해당 스레드 제어권을 포기합니다
2. async 작업 및 같은 블록 내의 코드를 스래드 제어권을 유지하며 바로 실행 못 할 수 있습니다.
3. 스레드 제어권이 넘어가면 시스템은 작업들을 스케쥴링함.
4. 시스템은 더 중요한 작업을 먼저 실행할 수 있습니다.
5. 시스템은 해당 작업을 실행할 차례라고 판단되면 resume하여 해당 작업에 스레드 제어권을 부여하고 작업을 실행합니다.
가 되겠네요.
스레드의 사용 권한을 기다린다고 생각해보면 await 이라는 키워드는 잘어울리는것같습니다.
또한 스레드를 양보한다는 의미의 yielding이라고도 불린다고 합니다.
또한 Codeblock이 한번에 처리되지 않을 수 있다는 것을 인식해야 하므로, 다른 작업들이 먼저 실행되는 동안 앱의 상태가 변할 수도 있다는 것을 인지해야합니다.