Skip to content

Instantly share code, notes, and snippets.

@sigridjineth
Created December 3, 2023 01:10
Show Gist options
  • Save sigridjineth/cfe2308c44b1afdae1391d23cd2a03bb to your computer and use it in GitHub Desktop.
Save sigridjineth/cfe2308c44b1afdae1391d23cd2a03bb to your computer and use it in GitHub Desktop.
Structured Concurrency

Structured Concurrency

Introduction

image

  • 예전의 프로그래밍 언어는 control flow가 상하로 왔다갔다 하였고 이런 코드는 흐름을 읽는 것을 방해함
  • 하지만 요즘은 구조화된 프로그래밍 방법을 통해 이를 쉽게 읽을 수 있음
  • 이러한 것이 가능하게 된 것은, block을 사용했기 때문이다.
  • block 안에서는 변수가 살아있고, 그 scope를 벗어나게 되는 경우 변수는 사라진다.
  • 이런 static scope와 structured programming 방법은, 변수의 life time과 제어문을 이해하기 쉽게 만들었다.
  • 이렇게 structured programming 방식은 이미 우리에게 상당히 익숙하다.
  • 하지만 요즘의 program은 비동기, concurrent code가 많아졌다. 이런 부분에 있어서 structured 한 방식으로 처리하는 것이 매우 어려웠다.

Swift Code

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..
            }
        }
    }
}
  1. Error 처리라는 structured 방식을 사용할 수 없음
  2. 네트워크 처리를 통해 데이터를 받아올 때, 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에서 비동기 처리가 끝날 때까지 기다려야만 한다.

Async-let Tasks

image

  • Code를 Concurrent하게 실행시키 위한 새로운 비동기 방식

  • Tasks들은 효율적이고, 안전하다고 판단되는 경우에 자동으로 Parallel하게 동작함

  • Task는 Swift와 깊게 통합되어 있기 때문에 compiler가 concurrency 버그를 탐지해줌

  • async function을 단순히 호출하는 것으로 Task가 생기는 것이 아니며 명시적으로 Task내부에 해당 함수를 넣어주어야 한다.

  • Swift에서는 데이터가 받아오는 시간동안에 다른 작업을 처리하고 싶울 땨 async let을 사용하면 된다.

    • 이를 사용하기 위해서는 뒤의 호출하는 함수(URLSession.shared.data(~))가 async 함수여야 한다.

image

  • Concurrent Binding 평가 방식은 다음처럼 이루어진다.
  1. 이전 상태에서 Child Task를 만든다.
  2. Child Task안에서 async let으로 async 함수를 호출
  3. Parent Task(이전 상태에서 사용하던 Task)를 위해 result에 placeholder를 할당
  4. 실제 동작(URLSession.shared.data())은 Child Task에서 수행
  5. Parent Task는 네트워크 결과를 기다리지 않고 진행한
  6. 하지만 실제로 Parent Task가 다운로드된 값을 필요로 한다면, await를 통해 child Task의 동작을 대기
  7. 만약 Error를 던지는 async 함수라면, try를 통해 받아주기
  8. 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

Reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment