[WWDC21] Explore structured concurrency in Swift

WWDC21 영상보면서 이해한대로 대충 끄적여보기 첼린지

정확한 이해가 필요하신 분들은 영상을 직접 시청하는 것을 권합니다.

컴플리션 헨들러로 비동기처리 cannot use error handling! cannot use a loop!

아 코드로 제공해주는거 달달하구만 나이스샷

  • 1분 57초
    func fetchThumbnails(
      for ids: [String],
      completion handler: @escaping ([String: UIImage]?, Error?) -> Void
    ) {
      guard let id = ids.first else { return handler([:], nil) }
      let request = thumbnailURLRequest(for: id)
      let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
          guard let response = response,
                let data = data
          else {
              return handler(nil, error)
          }
          // ... check response ...
          UIImage(data: data)?.prepareThumbnail(of: thumbSize) { image in
              guard let image = image else {
                  return handler(nil, ThumbnailFailedError())
              }
              fetchThumbnails(for: Array(ids.dropFirst())) { thumbnails, error in
                  // ... add image to thumbnails ...
              }
          }
      }
      dataTask.resume()
    }
    

async <- 이걸 어싱크가아니라 에이싱크라고 읽네..

원래쓰던 컴플리션 방식에선 throws로 에러처리가 불가능하니까 cannot use error handling이라고 한듯

async await에선 throws로 에러처리 가능!

  • 2분 56초
    func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
      var thumbnails: [String: UIImage] = [:]
      for id in ids {
          let request = thumbnailURLRequest(for: id)
          let (data, response) = try await URLSession.shared.data(for: request)
          try validateResponse(response)
          guard let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: thumbSize) else {
              throw ThumbnailFailedError()
          }
          thumbnails[id] = image
      }
      return thumbnails
    }
    

와… 🤩 쩐다잉 async let도 되네.. 오우..

  • 7분 59초
  • structured concurrency with async-let
    func fetchOneThumbnail(withID id: String) async throws -> UIImage {
      let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
      async let (data, _) = URLSession.shared.data(for: imageReq)
      async let (metadata, _) = URLSession.shared.data(for: metadataReq)
      guard let size = parseSize(from: try await metadata),
            let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
      else {
          throw ThumbnailFailedError()
      }
      return image
    }
    

뭔가 작업 단위를 부모, 자식 이런식으로 생각함. 트리 느낌? 저 fetchOne이라는 function을 큰 부모라고 생각하면 data, metadata 요청하는게 자식.

뭐.. 그래서 자식 중에 하나가 실패하면 에러 헨들링을 한다..

cancellation은 cooperative하다

cancel한다고 즉시 멈추지 않음. cancellation은 어디서든 확인 가능 설계해라 니 코드를 cancellation과 함께 in mind

  • 11분 46초
    func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
      var thumbnails: [String: UIImage] = [:]
      for id in ids {
          try Task.checkCancellation()
          thumbnails[id] = try await fetchOneThumbnail(withID: id)
      }
      return thumbnails
    }
    

음.. 그니까 이게 그 Task가 만약에 cancel되었으면 에러를 던지는데.. 음… 그래서 Task가 뭔데용?

  • 12분 16초
    func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
      var thumbnails: [String: UIImage] = [:]
      for id in ids {
          if Task.isCancelled { break }
          thumbnails[id] = try await fetchOneThumbnail(withID: id)
      }
      return thumbnails
    }
    

이 방법도 있다.. if로 체크 가능 if쓰는거보다 checkCancellation이 더 깔끔해보인다

group으로 concurrency 처리하는게 있는데 봅시다~

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: Void.self) { group in
        for id in ids {
            group.async {
                // Error: Mutation of captured var 'thumbnails' in concurrently executing code
                thumbnails[id] = try await fetchOneThumbnail(withID: id)
            }
        }
    }
    return thumbnails
}

withThrowingTaskGroup으로 뭘 어쩐다는거니.. 음.. 뭔가 작은단위에 async 그룹을 만들고 결국 await하는 비동기를 묶어준거군..

썸네일 아니고 thㅓㅁ 네일

어.. 근데 위 코드가 잘못된거라고 하넹 thumbnails 딕셔너리에 다 비동기로 접근하는거면 동시 접근 시 문제가 생길 수 있음

그래서 뭐시냐 그.. @Sendable 이걸 쓴다는데 어디 한번 봅시다

mutable variables는 캡쳐 불가능 오직 value types, actors, classes 근데 class는 reference type아닌가

어.. 궁금하면 protect mutable state with Swift actors 보래용

  • 16분 32초
    func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
      var thumbnails: [String: UIImage] = [:]
      try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
          for id in ids {
              group.async {
                  return (id, try await fetchOneThumbnail(withID: id))
              }
          }
          // Obtain results from the child tasks, sequentially, in order of completion.
          for try await (id, thumbnail) in group {
              thumbnails[id] = thumbnail
          }
      }
      return thumbnails
    }
    

아 뭔차이냐.. 음.. group async로 dictionary를 동시 참조하는 현상이 일어나니까 for로 group 요소의 await 시점을 잡고 dictionary에 접근하는듯

다음 건 Unstructured tasks

몇몇 task는 non-async contexts로 부터 런치될 필요가 있다 몇몇 task는 single scope의 경계를 넘어서 살아있다

  • 20분 39초
    @MainActor
    class MyDelegate: UICollectionViewDelegate {
      func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
          let ids = getThumbnailIDs(for: item)
          async {
              let thumbnails = await fetchThumbnails(for: ids)
              display(thumbnails, in: cell)
          }
      }
    }
    

음.. 이것도 더 자세하게 알고 싶으면 Protect mutable state with Swift actors를 봐라..

await를 썼지만, 어.. willDisplay function 자체가 async function이 아니기 때문에 dose not support concurrency라고 한다

그래서 저 await를 써야하는 코드 부분을 async로 감싸주면~? 성공!

음.. main thread에서 async로 할당된 작업을 main thread queue에 넣고 실행

근데 async에 main thread로 지정된게 없는데 왜 main일까라는 생각을 했는데 @mainActor라는게 있구만.. 뭔가 이거로 제어를 하는듯?

저게 function 앞에다 쓴게 아니라 class 위에다 쓴거라서 음.. 그럼 class 자체가 다 main thread에서 동작한다 뭐 이런건가..

non-async function은 이런식으로 쓰는거구만 근데 canceled랑 awaited를 수동적으로 관리를 해줘야함 어떻게?

  • 22분 11초
    @MainActor
    class MyDelegate: UICollectionViewDelegate {
      var thumbnailTasks: [IndexPath: Task.Handle<Void, Never>] = [:]
        
      func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
          let ids = getThumbnailIDs(for: item)
          thumbnailTasks[item] = async {
              defer { thumbnailTasks[item] = nil }
              let thumbnails = await fetchThumbnails(for: ids)
              display(thumbnails, in: cell)
          }
      }
        
      func collectionView(_ view: UICollectionView, didEndDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
          thumbnailTasks[item]?.cancel()
      }
    }
    

    오.. task를 dictionary로 저장하는구만.. 그런 다음 didEndDisplay에서 cancel 호출

detached task..

  • 24분 09초
    @MainActor
    class MyDelegate: UICollectionViewDelegate {
      var thumbnailTasks: [IndexPath: Task.Handle<Void, Never>] = [:]
        
      func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
          let ids = getThumbnailIDs(for: item)
          thumbnailTasks[item] = async {
              defer { thumbnailTasks[item] = nil }
              let thumbnails = await fetchThumbnails(for: ids)
              asyncDetached(priority: .background) {
                  writeToLocalCache(thumbnails)
              }
              display(thumbnails, in: cell)
          }
      }
    }
    

asyncDetached라는게 음.. global queue가 연상되는군.. 혹시 writeTOLocalCache도 제공되는 함수인건가.. 이것도 뭐 group으로 묶어서 여러 task를 쪼개서 제어할 수 있다.

@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task.Handle<Void, Never>] = [:]
    
    func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = async {
            defer { thumbnailTasks[item] = nil }
            let thumbnails = await fetchThumbnails(for: ids)
            asyncDetached(priority: .background) {
                withTaskGroup(of: Void.self) { g in
                    g.async { writeToLocalCache(thumbnails) }
                    g.async { log(thumbnails) }
                    g.async { ... }
                }
            }
            display(thumbnails, in: cell)
        }
    }
}