[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)
}
}
}