- 예전의 프로그래밍 언어는 control flow가 상하로 왔다갔다 하였고 이런 코드는 흐름을 읽는 것을 방해함
- 하지만 요즘은 구조화된 프로그래밍 방법을 통해 이를 쉽게 읽을 수 있음
- 이러한 것이 가능하게 된 것은, block을 사용했기 때문이다.
- block 안에서는 변수가 살아있고, 그 scope를 벗어나게 되는 경우 변수는 사라진다.
- 이런 static scope와 structured programming 방법은, 변수의 life time과 제어문을 이해하기 쉽게 만들었다.
- 이렇게 structured programming 방식은 이미 우리에게 상당히 익숙하다.
- 하지만 요즘의 program은 비동기, concurrent code가 많아졌다. 이런 부분에 있어서 structured 한 방식으로 처리하는 것이 매우 어려웠다.
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)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else { // ❎: Error 처리를 사용할 수 없음
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 // ❎ loop 사용 불가
// add image..
}
}
}
}
- 그럼 비동기, concurrent 코드에 structured한 방식을 도입해보자.
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)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else { // ❎: Error 처리를 사용할 수 없음
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 // ❎ loop 사용 불가
// add image..
}
}
}
}
- Error 처리라는 structured 방식을 사용할 수 없음
- 네트워크 처리를 통해 데이터를 받아올 때, loop와 같은 structured 방식을 사용할 수 없음
func fetchThumbnails (for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UlImage] = [:]
for id in ids {
let request = try await 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
}
- 만약 thumbnail 이미지를 수천장 받아야 한다면 await에서 비동기 처리가 끝날 때까지 기다려야만 한다.
-
Code를 Concurrent하게 실행시키 위한 새로운 비동기 방식
-
Tasks들은 효율적이고, 안전하다고 판단되는 경우에 자동으로 Parallel하게 동작함
-
Task는 Swift와 깊게 통합되어 있기 때문에 compiler가 concurrency 버그를 탐지해줌
-
async function을 단순히 호출하는 것으로 Task가 생기는 것이 아니며 명시적으로 Task내부에 해당 함수를 넣어주어야 한다.
-
Swift에서는 데이터가 받아오는 시간동안에 다른 작업을 처리하고 싶울 땨 async let을 사용하면 된다.
- 이를 사용하기 위해서는 뒤의 호출하는 함수(URLSession.shared.data(~))가 async 함수여야 한다.
- Concurrent Binding 평가 방식은 다음처럼 이루어진다.
- 이전 상태에서 Child Task를 만든다.
- Child Task안에서 async let으로 async 함수를 호출
- Parent Task(이전 상태에서 사용하던 Task)를 위해 result에 placeholder를 할당
- 실제 동작(URLSession.shared.data())은 Child Task에서 수행
- Parent Task는 네트워크 결과를 기다리지 않고 진행한
- 하지만 실제로 Parent Task가 다운로드된 값을 필요로 한다면, await를 통해 child Task의 동작을 대기
- 만약 Error를 던지는 async 함수라면, try를 통해 받아주기
- Tasks들은 효율적이고, 안전하다고 판단되는 경우에 자동으로 Parallel하게 동작
-
async let와 같은 structured task를 사용하게 되면, 현재 동작하고 있는 function의 task의 child가 되어 동작
-
이 child task의 life cycle은 parent의 scope에 갇힘
-
Parent Task는 본인이 가진 Child Task들의 동작이 모두 종료되어야 비로소 종료될 수 있음
-
이 규칙은 "비정상적인 제어 흐름"에도 적용되어 하위 작업이 대기하는 것을 방지
-
하기와 같이 각 Task의 동작을 실제 받는 곳에서 대기하도록 수정
-
async let을 사용하면 데이터의 할당까지 대기하지 않고, 실제 사용하는 시점에 대기하여 받는 방식으로 처리하여 보다 효율적인 처리가 가능
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
}
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) // ✅: Child Task가 생성됨
async let (metadata, _) = URLSession.shared.data(for: metadataReq) // ✅: Child Task가 생성됨
guard let size = parseSize(from: try await metadata), ✅
let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else { ✅
throw ThumbnailFailedError()
}
return image
- https://velog.io/@wansook0316/Structured-Concurrency
- https://developer.apple.com/videos/play/wwdc2021/10134/
- https://developer.apple.com/videos/play/wwdc2023/10170/
- https://lewoudar.medium.com/anyio-all-you-need-for-async-programming-stuff-4cd084d0f6bd
- https://250bpm.com/blog:71/
- https://blog.neonkid.xyz/283