- 프로그래밍 언어의 초기에는 instructions의 순서대로 작성되어 가독성이 좋지 않았다.
- 하지만 오늘날의 언어는 structured programming을 통해 control-flow가 통일성(uniform)을 가지게 되어 가독성이 좋아졌다. (if - then 구문)
- Swift는 Static Scope를 사용한다.
Concurrency에서의 Structured Programming
- 썸네일 이미지를 가져오는 전통적인 방식의 비동기 처리 코드 (escaping closure 사용)
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/await 키워드를 이용한 structured code
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 }
- no nested
- 만약 위의 코드에서 수천개의 썸네일을 받아온다면? 한개의 썸네일씩 처리하는 것은 매우 비효율적이다.⇒ Concurrency 필요
Async-let tasks
- async-let binding
- Sequential Bindings
- 기존의 직렬 바인딩은 위의 사진의 화살표 순서대로 진행된다.
- 앞의 statement 이후에 Initializer(
URLSession.shared.data(…)
)을 evaluate
- 썸네일 다운로드를 끝내고 변수(
let result
)에 Assign
- 다음 statement 진행
- 앞의 statement 이후에 Initializer(
- 코드
func fetchOneThumbnail(withID id: String) async throws -> UIImage { let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id) let (data, _) = try await URLSession.shared.data(for: imageReq) let (metadata, _) = try await URLSession.shared.data(for: metadataReq) guard let size = parseSize(from: metadata), let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else { throw ThumbnailFailedError() } return image }
- 하지만, 썸네일을 다운로드하는 작업은 시간이 꽤 걸릴 수 있기 때문에 해당 작업을 진행하는 동안 다른 일을 할 수 있도록 하는 것이 효율적이다.
- 기존의 직렬 바인딩은 위의 사진의 화살표 순서대로 진행된다.
- Concurrent Bindings (with async-let)
- 진행 순서
- 앞의 statement 이후에 새로운 child task를 생성
- Initializer을 evaluate하는 것과 동시에 변수(result)에 placeholder assign
- 썸네일 다운로드가 진행되는 것과 동시에 다음 statement 수행
- 만약 실제로 result 값이 필요한 상황이 오면 parent task는 child task의 수행 완료를 기다린다. (await)
- 위의 예시처럼 error가 발생할 수 있는 코드에는 try를 붙인다.
- 코드
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 }
- async 키워드를 let 앞에 붙여서 concurrent 하게 작업을 수행할 수 있다.
- 위와 같은 Task 작업은 Task tree에 속하게 된다. (Structred concurrency의 중요한 부분)
- fetchOneThumnail 함수 수행에서의 task tree
- Parent Task는 Child Task가 모든 작업을 끝내야 자기 자신의 일을 끝낼 수 있다.
- 이러한 순서는 child task에서 error가 발생했을 때도 동일하게 동작한다.
- metadata를 불러오는 과정에서 error가 발생하더라도 곧바고 Parent task가 error를 발생시키는 것이 아니라 다른 child의 작업의 완료 되고 나서 error를 던진다.
- 단, 다른 child task에 unawaited를 부여하여 취소로 표기한다. 취소로 표기하여도 작업이 중지되는 것은 아니다.
- 단순히 그 결과게 더 이상 필요하지 않다는 것을 task에 알리는 것이다.
- task가 취소되면 해당 task의 하위 task도 자동으로 취소된다.
- metadata를 불러오는 과정에서 error가 발생하더라도 곧바고 Parent task가 error를 발생시키는 것이 아니라 다른 child의 작업의 완료 되고 나서 error를 던진다.
- 위와 같은 작업 방식의 보장은 ARC가 메모리 수명을 자동으로 관리하는 것처럼 작업 수명 관리를 도와 실수로 작업이 유출되는 것을 방지한다.
- 이러한 순서는 child task에서 error가 발생했을 때도 동일하게 동작한다.
- Cancellation checking
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 }
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 }
- 이렇게 작성하면 task가 도중에 cancel 되었을 때 오직 일부분의 딕셔너리 결과값만 함수의 반환값으로 return 된다. (이러한 함수를 작성한다면 사용자가 부분적으로만 결과를 얻을 수도 있다는 것을 알려야한다.)
- 진행 순서
Group tasks
- async-let task는 concurrency의 amount가 정해져 있을 때 사용된다. (static할 때)
- 위의
fetchThmbnails
예시에서 ids를 반복문의 다음 반복이 시작하기 전에 child task가 완료되어야 한다. 만약fetchOneThumbnail
작업들이 동시에 진행되도록 하고 싶다면 동적인 concurrency amount를 위한 장치가 필요하다.
- 위의
- Group tasks는 concurrency의 dynamic amount를 위해 설계되었다.
- 코드
위와 같이 코드를 작성하면 data race가 생기기 때문에 컴파일러는 에러를 발생시킨다. 둘 이상의 child tasks가 썸네일을 동시에 thumnails 딕셔너리에 insert를 시도할 경우 충돌이 발생하기 때문이다.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
- data race 문제를 해결한 코드
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 }
- data race 문제를 해결한 코드
Unstructured tasks
- 모든 task가 structured pattern에 적합한 것은 아니다.
- Some tasks need to laucn from non-async contexts
- Some tasks live beyond the confines of a single scope
- Unstructured tasks
- origin context의 actor isoloation과 priority를 물려받는다.
- lifetime은 어떤 범위에 국한되지 않는다.
- 어디에서나 실행할 수 있다. (non-async function 에서도 실행 가능)
- 수동으로 cancel 하거나 await 해야한다.
- 예시 코드
@MainActor class MyDelegate: UICollectionViewDelegate { func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) { let ids = getThumbnailIDs(for: item) Task { let thumbnails = await fetchThumbnails(for: ids) display(thumbnails, in: cell) } } }
Detached tasks
- Detached tasks
- Unscoped lifetime, manually cancelled and awaited
- origin context로 부터 어떠한 것도 물려받지 않는다.
- Opitonal parameters controls priority and other traits
- 코드 예시
@MainActor class MyDelegate: UICollectionViewDelegate { var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:] func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) { let ids = getThumbnailIDs(for: item) thumbnailTasks[item] = Task { defer { thumbnailTasks[item] = nil } let thumbnails = await fetchThumbnails(for: ids) Task.detached(priority: .background) { writeToLocalCache(thumbnails) } display(thumbnails, in: cell) } } }
요약
Uploaded by
N2T'iOS > Swift' 카테고리의 다른 글
ARC in Swift: Basics and beyond (0) | 2023.04.13 |
---|---|
Understanding Swift Performance (2) | 2023.03.13 |
Meet AsyncSequence (0) | 2023.02.11 |
Use async/await with URLSession (0) | 2023.02.11 |
Meet async/await in Swift (0) | 2023.02.11 |
댓글