Async / Await && Actor

준우·2023년 12월 10일
0

Swift 이야기

목록 보기
1/19
post-thumbnail

Async / Await 란?

  • iOS 13부터 사용 가능
  • 비동기처리를 위해 Completion Closure 를 사용하지 않고, Async / Await 키워드를 사용하여 더욱 가독성 있는 비동기 프로그래밍이 가능하게 해줍니다.
  • Async 사용 예시
// Example1
func fetchItems() async { }

// Example2
func fetchSomeItems() async throws -> String { }
  • Await 키워드를 사용하는 쪽에서 호출하는 예시
await fetchItems()
await fetchSomeItems() 

Task란?

  • Async 작업의 단위입니다.

  • 스위프트에서 DispatchQueue 나 OperationQueue, Thread 따로 없이 작성하면 main thread 인 sync(=동기)로 동작합니다. 이때, Task 블록으로 감싸서 async 코드를 수행할 수 있도록 제공해줍니다.

  • Task 사용 예시

Task {
	await fetchItems()
}

// 물론, Task 를 사용하지 않고도 Async 함수를 사용할 수 있습니다.
// 만약 Task 블록 없이 async 함수를 호출하려면, async 함수를 호출하는 쪽도 async여야 가능합니다.
// viewDidLoad()는 override 함수라, async를 붙이지 못하지만 이론상 async를 붙이면 Task 블록 없이도 호출 가능합니다.
// Task 를 사용하지 않은 Async 사용 예시

override func viewDidLoad() async {
  super.viewDidLoad()
  
  await fetchItems()
}
  • Await 실행 순서
 override func viewDidLoad() {
    super.viewDidLoad()
    
    print("1. will enter task block")
    Task {
      print("2. did enter task block")
      // await를 만남 -> 다음 라인의 코드 실행시키지 않고 대기
      try? await Task.sleep(for: .seconds(10)) 
      print("3. will out task block")
    }
    print("4. some another code")
  }

/*
1. will enter task block
4. some another code
2. did enter task block
3. will out task block

1 - 4 - 2 - 3 순으로 실행됨을 확인할 수 있습니다.
*/

Task 와 DispatchQueue 스레드 차이

  • 공통점
    • DispatchQueue 블록 안에 들어가면 임의로 스레드로 처리합니다.
    • Task 블록 안에서도 임의의 스레드로 처리합니다.
  • 차이점
    • Task 블록 안에서 코드가 대기하여 다음 코드를 실행하지는 않지만, 내부적으로 스레드는 Block 되지 않고 다른 일을 처리할 수 있도록 효율적으로 설계 합니다.
  • Task 를 사용한 예시
  override func viewDidLoad() {
    super.viewDidLoad()
    
    Task {
      try? await Task.sleep(for: .seconds(10)) 
      // await를 만남 -> 다음 라인의 코드 실행시키지 않고 대기함
    }
  }
  • GCD 를 사용한 예시
override func viewDidLoad() {
    super.viewDidLoad()
    
  	// DispatchQueue 는 해당 Thread 가 점유하고 일을 하고 있으면, 그 스레드는 Block 되어 해당 일을 끝낼때까지 다른 일 처리를 못하는 상태입니다.
    DispatchQueue.global().async {
      self.fetchHeavyItems()
    }
  }
  
  func fetchHeavyItems() {
    // get some heavy items
  }

Async let 사용 방법 (Concurrency)

  • Await 로 데이터를 얻지 않고, Async 로 프로퍼티를 선언하여, 동시성 처리에 사용

  • Async let 사용 예시

// Async let 선언 방법
async let a = getA()
  • Async 메소드 예제
func getA() async -> Int {
  try? await Task.sleep(for: .seconds(3))
  return 3
}
  
func getB() async -> Int {
  try? await Task.sleep(for: .seconds(5))
  return 5
}
  • Async let 없이 호출할 경우 예시
// getA, getB 두 개 모두 동시에 실행가능하지만, getA에서 3초 대기 후 getB에서 5초 대기하여 실행, 최종 8초간 대기하는 비효율적인 코드
Task {
	let a = await getA()
	let b = await getB()
	print(a + b)
}
// 최종 : 8초
  • Async let 사용하여 호출할 경우 예시
Task {
	async let a = getA()
	async let b = getB()
	let sum = await (a + b)
	// 비동기 바인딩
	print(sum)
}

Actor 란?

  • 스위프트에서 Deadlock, Race Condition 문제를 해결하기 위해 만들어낸 자료형
  • Class 와 같이 Reference Type 이고 Actor 는 한 번에 하나의 작업만 액터의 상태를 변경할 수 있는 형태입니다.
  • 사용 방법은 class 로 정의하고, 초기화해서 사용합니다.
  • 사용 예시
// actor 사용 방법
actor Counter {
  private let name: String
  private var count = 0
  
  init(name: String) {
    self.name = name
  }
    
  func plus() { 
    count += 1 
  }
}

let counter = Counter(name: "counter")

Deadlock 이란?

  • 둘 이상의 프로세스가 다른 프로세스가 점유하고 있는 자원을 서로 기다릴 때 무한 대기에 빠지는 상황입니다.
  • 다음 아래 4가지 조건이 모두 충족되면 Deadlock 발생합니다.
    • Mutual Exclusion : 자원 하나는 한 프로세스만 사용 가능
    • Hold and Wait : 하나 이상의 자원을 점유하면서 다른 프로세스가 점유하고 있는 자원을 대기하는 상태
    • No preemtion : 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 뺏을 수 없습니다.
    • Circular wait : 자원을 요구하며 대기하는 순환적인 형태입니다.

Actor 사용 방법

  • Actor 형태 예시
actor Counter {
  private name: String
  private var count = 0
  
  init(name: String) {
    self.name = name
  }
    
  func plus() {
    count += 1
  }
}
  • 일반적인 인스턴스 호출하듯이 actor 의 메소드를 호출할 수 없습니다.

  • Actor 의 메소드 호출 예시

  • Task, Await 키워드를 사용하면 됩니다.

let counter = Counter(name: "counter")
    
Task {
  await counter.plus()
}

Nonisolated 란?

  • let 키워드로 선언한 actor 는 읽기만 가능합니다. 또한, Race Condition 이 발생하지 않는 것을 보장합니다.

  • 메소드 앞에 Nonisolated 를 붙여서 사용하면, Task, Await 키워드 없이 사용할 수 있습니다.

  • 사용 예시

// nonisolated 키워드 예시
actor Counter {
  private let name: String
  private var count = 0
  
  init(name: String) {
    self.name = name
  }
    
  func plus() {
    count += 1
  }
  
  nonisolated func getName() -> String { // <-
    name
  }
}

// 사용하는쪽
let counter = Counter(name: "counter")
counter.getName() // Task, await 키워드 없이 접근 가능

@MainActor 란?

  • 항상 main 에서 동작하는 actor 를 의미합니다.

  • 항상 메인 스레드에서 실행되어야 하는 클래스입니다.

  • @MainActor 를 사용하여 구현합니다.

  • 사용 예시

@MainActor
class SomeClass { }
  • 특정 메소드만 메인 스레드에서 실행되어야 한다면, 해당 메소드 위에 @MainActor 키워드를 사용하면 됩니다.
class SomeClass {
  @MainActor
  func runSomeMethod() {
 			// Do stuff 
  }
}
  • 스위프트 5.5 에서는 UIKit, SwiftUI 구성 요소 모두 MainActor 입니다. 그렇기 때문에 백그라운드 작업이 완료된 후 UI를 업데이트 하려고 할 때, DispatchQueue.main.async { } 로 수동 업데이트 할 필요없이 자동으로 메인에서 실행됩니다.

Async / Await 를 이용하여 API 를 호출해보자!

  • AlbumResult 모델
struct AlbumResult: Codable {
  let results: [Album]
}

struct Album: Codable, Hashable {
  let collectionId: Int
  let collectionName: String
  let collectionPrice: Double
}
  • Completion Closure 를 사용한 예시
enum APIError: Error {
  case invalidURL
  case noData
}

enum API {
  static func fetchAlbums(completion: @escaping (Result<AlbumResult, Error>) -> Void) {
    guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=album") else {
      completion(.failure(APIError.invalidURL))
      return
    }
    
    URLSession.shared.dataTask(with: url) { data, _, error in
      if let error = error {
        completion(.failure(error))
        return
      }
      
      guard let data = data else {
        completion(.failure(APIError.noData))
        return
      }
      
      do {
        let result = try JSONDecoder().decode(AlbumResult.self, from: data)
        completion(.success(result))
      } catch {
        completion(.failure(error))
      }
      
    }
    .resume()
  }
}
  • API 함수 사용한 예시
API.fetchAlbums { result in
  switch result {
  case let .success(response):
    print(response)
  case let .failure(error):
    print(error)
  }
}

Async, Await 를 사용한 경우

  • URLSession 앞에 Await 키워드를 붙여 사용하면 됩니다.

  • 사용 예시

// MARK: Async & Await
static func fetchAlbums() async throws -> AlbumResult {
  guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=album") else {
    throw APIError.invalidURL
  }
  let (data, _) = try await URLSession.shared.data(from: url)
  let result = try JSONDecoder().decode(AlbumResult.self, from: data)
  return result
}
  • API 함수 사용한 예시
// async await
Task {
  do {
    let result = try await API.fetchAlbums()
    print(result)
  } catch {
    print(error)
  }
}

0개의 댓글