Async/await & Task

Hyeongseok Yun·2024년 1월 22일
0

Swift

목록 보기
1/1
post-thumbnail

현업에서 비동기 통신을 GCD(Grand Central Dispatch)로 작성 했었는데,

스레드 관리 completion hadnler 처리 등 코드가 길어지고 작성하기 매번 번거로워 Async/await 도입을 계획하였다.

도입하기 전, 해당 내용에 대해 정리하고 실무에 적용하고자 한다.


GCD → Async & await 개선사항

기존 GCD의 문제

  • thread explosion로 인한 메모리 오버헤드 및 과도한 컨텍스트 스위치 발생
  • 중첩된 completion handler 및 까먹을 수 있는 completion handler 처리

completion(.)을 작성하지 않아도 컴파일에 문제가 없다.


해당 문제점을 해결하고자 Swift 5.5에서 async/await이 등장하였다.
async & awiat은 비동기 코드를 마치 동기 코드인것 처럼 작성 할 수 있으므로, GCD에 비해 가독성이 매우 좋다.


Async/await 적용된 코드

-> return or throw 둘 중 하나를 반드시 해아하므로 completion handler를 까먹을 수 있는 문제점을 개선 할 수 있다.

-> FIFO이므로, URLSession의 response를 다른 작업에 사용 할 수 있다. (마치 동기 코드인것 처럼)

-> 클로저와 들여쓰기가 사라짐으로, 가독성이 더 좋아졌다.


Async/await

먼저 관련 용어를 알아보자.

Async란?

비동기 작업(ex: API 호출)이 끝나는 것을 기다리지 않고 다음 작업을 수행하도록 처리하는 것

Await란 ?

기다린다 or 일시중단한다. 특정 작업을 기다리면서 다른 작업을 수행 할 수 있도록한다.

즉, Async/await은 비동기 작업 환경에서 특정 작업을 기다리면서, 다른 작업을 수행할 수 있다.

함수 이름 뒤에 async가 붙으면 이 함수는 비동기라는 것을 나타낸다.

func asyncFuncFirst() async {
      
      await ImageService().fetchThumbnail(for: "")
      // 이 안의 동작은 비동기로 실행
 }

async 함수를 호출하기 위해서는 호출하는 함수도 async 또는 Task {}로 되어 있어야한다.
그리고 await 키워드를 사용한다.

// async 함수
func excuteAsyncFunc() async {
        
    await asyncFuncFirst()
}

/// 일반 함수 - Task 사용
func excuteAsyncFunc() {
        
     Task {
         await asyncFuncFirst()
     }
}

여기서 await이 표시된 곳은 잠재적인 일시 중단 지점(potential suspension point)이다.
await을 마주치면 스레드가 다른 동작을 수행 할 수 있도록 스레드 제어권을 시스템에게 양도한다.
자세한건 밑 스레드 제어권 설명에서 확인하자.
(await 키워드가 있다고 무조건 suspend 되는것은 아니다. await 타겟 함수 내부에 await이 존재하지 않으면
suspend 되지 않는다.)

사용법

async 함수 정의

func asyncFuncFirst() async {
      
      await ImageService().fetchThumbnail(for: "")
      // 이 안의 동작은 비동기로 실행
 }
 
 func asyncFuncSecond() async {
      
      await ImageService().fetchThumbnail(for: "")
 }
 
  func asyncFuncThird() async throws -> Bool {
      
      await ImageService().fetchThumbnail(for: "")
      return true
      // .fetchThumbnail() 정상적으로 완료되면 return 되고, 실패하면 throw 한다.
 }

async 함수 호출


 // async 함수
 func executeAsyncFunc() async {
      
      await asyncFuncFirst()
      await asyncFuncSecond()
      // asyncFuncFirst() 완료 후에 asyncFuncSecond() 실행
      // async 함수를 호출하기 위해서는 해당 함수도 async여야 한다.
 } 
 
 // 일반 함수 - Task 사용 (순서대로)
 func executeAsyncFunc() async {
 
 	  Task {
         await asyncFuncFirst()
         await asyncFuncSecond()
      }
      // asyncFuncFirst() 완료 후에 asyncFuncSecond() 실행
      // Task {} 를 사용하여 수동적으로 concurrent 환경을 생성 할 수 있다.
 } 
 
 // 일반 함수 - Task 사용 (한번에)
 func executeAsyncFunc() async {
      
      Task {
         async let _ = await asyncFuncFirst()
         async let _ = await asyncFuncSecond()
      }
      // asyncFuncFirst(), asyncFuncSecond() 동시에 실행
      // async let은 concurrent 환경에서 사용 가능하다. (async, Task {} 어디서든 사용 가능)
 } 
 
 // async 함수 - 에러 처리 및 비동기 함수 return 값 활용
 func executeAsyncFunc() async {
      
      do {
      	 let result = await asyncFuncThird()
         print(result)
         
      } catch {
         ...
      }
      // asyncFuncThird()의 return 값을 활용 할 수 있다.
      // asyncFuncThird() 실패 시 catch {} 에서 컨트롤 가능
 } 

스레드 제어권

1. Sync

A 함수, A 함수 내부에 B 함수가 존재한다고 가정해보자.

A 함수에서 B 함수를 호출하면, A 함수에서 실행되던 스레드 제어권을 B 함수에 전달한다.

B 함수가 끝날때까지 해당 스레드는 완전히 점유되어서, 다른일을 수행하지 못하게 된다.

B 함수가 끝나면 다시 A 함수에게 스레드에 대한 점유권을 넘겨준다.

    1. fetchThumbnail 함수 호출

  1. fetchThumbnail 함수 내부의 thumbnailURLRequest 함수가 호출되면, 스레드 제어권을 thumbnailURLRequest 함수에 전달

  2. thumbnailURLRequest 함수가 완료되면, 스레드 제어권을 다시 fetchThumbnail 함수에게 돌려줌


2. Async

A 함수, A 함수 내부에 async B 함수가 존재한다고 가정해보자.

A 함수에서 B 함수를 호출하면, A 함수에서 실행되던 스레드 제어권을 B 함수에 전달한다.

B 함수는 async 함수이므로, 중간에 suspend 될 수 있다. (이때 원래 A도 suspend 된다.)

suspend 된다는 것은 스레드 제어권을 시스템에게 양도하는 것이므로, 시스템은 해당 스레드를 사용하여

우선순위가 높은 다른 작업을 수행할 수 있게 된다.

시스템이 우선 순위를 판단해가며 여러 작업들을 실행하다가, 일시 중단된 비동기 함수를 다시 실행하는 작업

가장 중요하다고 판단할 때가 온다. 이때 해당 함수를 재개(resume)하고, **시스템이 해당 함수에게 스레드 제어권을

전달하여 작업을 계속 할 수 있다.
    1.
fetchThumbnail 함수** 호출

  1. fetchThumbnail 함수 내부의 data 함수 호출 및 스레드 제어권 전달
       (data 함수는 await 으로 되어있으므로, 스레드 제어권을 시스템에게 양도)

  2. 시스템은 우선순위를 판단해가며 다른 작업을 수행

  3. 시스템이 data 함수를 계속 실행하는 작업이 가장 중요하다고 판단할 때,
         스레드 제어권 전달 및 data 함수 재개(resume)



P.S

await 전/후 스레드는 동일하다는 보장이 없다.

디바이스 코어 수만큼 스레드가 생성된다. (thread explosion 방지 및 context switching이 적음)


Task

Task란?

비동기 작업의 단위 (A unit of asynchronous work).

Task {} 로 일반 함수에서 비동기 컨텍스트에 접근 할 수 있다.

비동기 코드를 캡슐화 할 수 있을 뿐만 아니라, 코드의 실행 및 취소를 제어 할 수 있다.

== global dispatch queue에서 async 호출


Task임의의 스레드에서 다른 Task와 함께 동시에 실행된다.

Task'바다위의 독립적인 보트'라고 생각하면 이해가 쉽다.

독립적으로 수행되지만, 수행된 데이터를 다른 Task에 공유 할 수도 있다.


데이터 공유

Task들이 서로의 데이터를 공유할려면, Task의 return 값을 변수에 지정하여

다른 Task에서 해당 return을 await한다.


우선순위 (Priority)

Task에는 GCD의 QoS(Quality of Service)와 같은 역활을 하는 TaskPriority가 있다.
아래는 Qos와 TaskPriority를 서로 상응하는 우선순위에 맞게 배치한 표이다.

QosTaskPriority
UserinitiatedHigh
DefaultMedium
UtilityLow
  • Userinitiated, High - 사용자가 적극적으로 기다리고 있는 작업에 적합하다. (UI 인터렉션)

  • Default, Medium - 사용자가 적극적으로 기다리지 않는 대부분의 작업에 적합하다.

  • Utility, Low - 파일 복사나 데이터 가져오기 등 긴 작업에 적합하다.






참고

https://developer.apple.com/videos/play/wwdc2021/10095/
https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f

profile
운동과 개발을 좋아합니다. 💪 🧑🏻‍💻

0개의 댓글