[TIL]11.27

rbw·2022년 11월 27일
0

TIL

목록 보기
52/97

동시성, 여러 장의 이미지를 동시에 가져오기

참조

https://www.swiftbysundell.com/articles/async-and-concurrent-forEach-and-map/

https://www.swiftbysundell.com/articles/swift-concurrency-multiple-tasks-in-parallel/

https://developer.apple.com/documentation/swift/withthrowingtaskgroup(of:returning:body:)


GCD를 공부하면서 느끼기도 했고, 이미지를 서버에서 들고오는 작업을 하면서 깨달았는데 현재 진행하고 있는 프로젝트에서는 이미지를 들고오는 로직이 Task 내부에 하나의 블록에 묶여있다.

// 현재 프로젝트에서의 image fetch 하는 로직
self.imageRequestTask = Task {
for i in 0..<curation.descriptions.count {
      if let image = try? await ImageCache.shared.load(curation.descriptions[i].image) {
          self.images.append(image)
      } else {
          self.images.append(UIImage())
      }
  }
}

이는 적절한 처리를 따로 하지 않는다면 결국 순차적으로 이미지를 들고오게되는데, 그렇게 된다면 병렬로 이미지를 들고오지 않는다 라는 의미가 된다. 다행히 병렬로 이미지를 들고올 수 있게 하는 메소드가 존재하였다

withThrowingTaskGroup(of:returning:body:) 메소드

func loadImages(URLs: [String]) async throws -> [String : UIImage] {
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
        for url in URLs {
            group.addTask {
                let image = try? await ImageCache.shared.load(url)
                return (url, image ?? UIImage())
            }
        }
        var images = [String : UIImage]()
        
        for try await (url, image) in group {
            images[url] = image
        }
        return images
    }
}

withThrowingTaskGroup 메소드가 존재한다 of 에는 하위 Task의 결과 타입을 넣어주고, 클로저로 원하는 액션을 전달하면 된다. 그리고 하위 Task를 얻어오려면 for await in 구문을 활용해서 들고 오면 된다.

위 로직은 딕셔너리가 필수인데 왜냐하면, 이미지 한장을 들고오는 Task 들이 group에 여러개 존재합니다. 그리고 이를 실행하는데 완료가 되는 순서는 뒤죽박죽입니다.(동시로 진행하기 때무네!)

그런 이유로, 데이터가 순차적일 확률이 매우매우 낮기 때문에 딕셔너리를 사용해서 원하는 이미지를 원하는 위치에 넣어줘야 합니다. (순서가 중요하지 않다면 필요는 없겠쪄 ~?)

아직 이 메소드에 대해서 더 공부를 해야 함니다,,,

concurrentMap, asyncMap 사용

Sequence 프로토콜에 확장을 하여 사용을 하면 됩니다.

extension Sequence {
    func concurrentMap<T>(
        _ transform: @escaping (Element) async throws -> T
    ) async throws -> [T] {
        let tasks = map { element in
            Task {
                try await transform(element)
            }
        }
        return try await tasks.asyncMap { task in
            try await task.value
        }
    }
}

extension Sequence {
    func asyncMap<T>(
        _ transform: (Element) async throws -> T
    ) async rethrows -> [T] {
        var values = [T]()

        for element in self {
            try await values.append(transform(element))
        }

        return values
    }
}

func loadImageArray(URLs: [String]) async throws -> [UIImage] {
  let concurrentImages: [UIImage] = try await URLs.concurrentMap { url in
      return try! await ImageCache.shared.load(url) ?? UIImage()
  }
  return concurrentImages
}

저도 아직 완전한 이해는 안되었고 사용법정도만~

간단히 설명하자면, 맨 처음 방식과 비슷합니다. Task들을 담아서 동시에 실행합니다. 그리고 tasks에 담은 순서대로 반환을 합니다.

맨 처음 방식과의 차이로는 위 방식은 순서가 보장된다는 차이가 있습니다. asyncMap을 활용하여 각 Task들이 원래의 순서에 맞게 반환이 되기 때문에 딕셔너리를 사용하지 않아도 되는 장점이 있습니다


확실히 위의 로직들을 프로젝트에 적용을 하고 비교를 해보니 사진의 용량이 많이 작고, 개수도 적은데도 눈에 보이는 속도의 차이가 존재하여서 적용하기 잘했다 라는 생각을 했습니다 ㅎㅎㅎ

근데 아직 배울게 많네요 하ㅏㅏㅏ

profile
hi there 👋

0개의 댓글