-
Swift Concurrency - ParallelConcurrency 2023. 3. 6. 08:56
안녕하세요. 그린입니다🍏
이번 포스팅에서는 Swift Concurrency Task의 소개에 이어 여러 Task를 병렬로 처리하는 방법에 대해 학습해보겠습니다🙌
우리가 Swift Concurrency를 사용하는 가장 근본적인 이유는 비동기 처리를 다루는것도 있지만 이걸 다루는 이유 자체가 비동기 작업을 병렬로 대부분 수행하기에 이점을 가져가기 위함도 있을거에요!
즉, Swift에 내장된 동시성 시스템의 이점 중 하나로 비동기 작업의 병렬 처리를 꼽을 수 있습니다.
그럼 비동기 작업을 병렬로 처리하는 몇가지 방법에 대해 알아보시죠🕺🏻
비동기 작업을 동시성으로 처리하기
어떤 한 클래스에 다양한 비동기 작업을 수행하는 메서드가 있다고 생각해볼께요.
class ProductLoader { ... func loadFeatured() async throws -> [Product] { ... } func loadFavorites() async throws -> [Product] { ... } func loadLatest() async throws -> [Product] { ... } } extension Product { struct Recommendations { var featured: [Product] var favorites: [Product] var latest: [Product] } }
이런 코드를 가정해봅시다!
즉 3가지의 메서드를 통해 전체 상품의 정보를 로드해주죠.
그렇다면 우리는 상품이라는 객체 모델을 만들기 위해서 저 3가지의 작업을 끝내야 합니다.
아주 간단하게 우리가 Swift Concurrency를 처음 접했을때와 같이 async한 비동기 메서드를 사용하는 부에서 await 키워드를 사용해 호출하면 됩니다.
이런 구현을 위 ProductLoader 내부에 두고 Reccomendations 인스턴스를 생성하여 반환하면 되겠죠?
extension ProductLoader { func loadRecommendations() async throws -> Product.Recommendations { let featured = try await loadFeatured() let favorites = try await loadFavorites() let latest = try await loadLatest() return Product.Recommendations( featured: featured, favorites: favorites, latest: latest ) } }
요렇게 비동기 작업을 순차적으로 실행할 수 있습니다.
즉, 여기의 loadRecommendations 메서드가 해당 앱의 다른 부분의 코드와 동시에 수행되고는 있지만 실제로 내부 작업 세트를 수행하기 위해 동시성을 활용하지는 못하고 있습니다.
즉, 내부의 세가지 메서드의 순서가 현재는 순차적으로 끝나야 실행되는 구조인데 사실 이 경우에는 전혀 그럴 필요가 없음으로 내부에서도 동시에 수행될 수 있어야 합니다.
만약 각 await를 사용하지 않고 아래처럼 해줄 수 도 있습니다.
extension ProductLoader { func loadRecommendations() async throws -> Product.Recommendations { try await Product.Recommendations( featured: loadFeatured(), favorites: loadFavorites(), latest: loadLatest() ) } }
이 경우에는 코드가 훨씬 줄어들어 이점은 있지만 위 코드와 마찬가지로 동시성을 갖진 않습니다.
완전히 순차적으로 실행됩니다.
그럼 이런 익숙한 비동기의 순차적 처리를 비동기의 동시 처리로 어떻게 바꿔볼 수 있을까요?
extension ProductLoader { func loadRecommendations() async throws -> Product.Recommendations { async let featured = loadFeatured() async let favorites = loadFavorites() async let latest = loadLatest() return try await Product.Recommendations( featured: featured, favorites: favorites, latest: latest ) } }
우리는 이걸 해결하기 위해서 비동기 바인딩을 사용해 동시성 시스템에 각각의 로드 작업을 병렬로 수행하도록 지시해야 됩니다.
그걸 위해 async let 구문을 사용하여 해결해줄 수 있습니다!
즉, 완료될 때까지 기다릴 필요가 없이 백그라운드에서 비동기 작업을 시작해주죠.
async let 처리 후 실제 해당 데이터를 사용할 부분에서 await로 호출하여 사용해주면 끝입니다!
그럼 해당 비동기 작업들을 병렬로 처리하고자 하는 니즈를 충족시켜주죠🙌
이렇게 유한한 작업의 실행에서는 이런 방식을 쓸 수 있지만 만약 그렇지 않을때는 어떻게 해야 할까요?
Task 그룹
class ImageLoader { ... func loadImage(from url: URL) async throws -> UIImage { ... } }
보시면 ImageLoader와 같이 네트워크를 통해 이미지를 로드해오는 Task가 있다고 가정해보죠.
즉 위 코드를 통해 하나의 이미지를 서버에서 받아올 수 있어요.
extension ImageLoader { func loadImages(from urls: [URL]) async throws -> [URL: UIImage] { var images = [URL: UIImage]() for url in urls { images[url] = try await loadImage(from: url) } return images } }
그렇지만 위와 같이 만약 몇개가 들어올지 모르는 정말 수많은 이미지를 받아와야할 수도 있습니다.
그럼 위에서 비동기 > 동시 작업으로 살펴봤던 것처럼 이미지를 순차적 다운로드가 아닌 위 loadImages 메서드를 동시에 실행하려고 해볼께요.
현재 for 루프 내에서 loadImage를 호출할 때 await를 사용하고 있으니까요!
그럼 아까처럼 async let을 사용하면 될까요?
아쉽지만 위의 코드는 컴파일 시 수행하는 작업의 수를 알 수 없기에 사용할 수 없습니다🥲
즉 다른 방법을 사용해야 하는데, 우리가 GCD를 할 때 접했던 group을 사용해줄 수 있습니다!
그룹을 구성하려면 Task 내 오류를 발생시키는 옵션을 사용할지 여부에 따라 withTaskGroup 혹은 withThrowingTaskGroup을 사용할 수 있습니다.
현재 경우에는 loadImage에서 throws를 하기에 후자를 택해야하죠.
그럼 한번 고쳐볼까요?
extension ImageLoader { func loadImages(from urls: [URL]) async throws -> [URL: UIImage] { try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in for url in urls { group.addTask{ let image = try await self.loadImage(from: url) return (url, image) } } var images = [URL: UIImage]() for try await (url, image) in group { images[url] = image } return images } } }
많이 변경된것 같죠?
처음 URL에 대해 for 문 내에서 그룹으로 묶어줘서 반복 작업을 수행합니다.
즉, 로드 작업이 하나씩 순차적으로 완료되기를 기다리는게 아니고 그룹별로 각 이미지 로드 작업을 추가하죠.
이제 로드 작업에 대해 순차적이 아닌 개별적으로 동시 실행할 수 있도록 변경되었습니다.
async let을 사용할때처럼 Task가 어떤 상태도 직접 변형하지 않는 방식으로 동시 코드를 작성하는 것의 이점을 가져갔습니다.
이렇게 된다면 모든 종류의 데이터 경합 문제를 완전히 방지하는 동시에 잠금 혹은 직렬화 코드를 혼합하여 도입할 필요가 없어지죠.
즉 각각의 동시 작업이 결과를 반환하도록 기다린 후 최종 데이터를 구성하기 위해 순차적으로 결과를 기다리는것이 좋은 접근 방식입니다.
물론 actor의 사용도 좋아요!
결론
우리가 틀을 깨야하는건 해당 메서드들이 비동기 메서드라고 해서 꼭 작업들을 동시에 수행하는 것은 아니라는 점을 기억해야 합니다.
그렇기에 순차적으로 실행할 수도 있어야 하고 만약 원한다면 해당 작업들을 의도적으로 병렬로 실행할 수도 있어야 합니다.
마무리
Swift Concurrency를 겪으면서 Task의 순차적 실행에 이점이 크구나만 느꼈었는데 확실히 이처럼 병렬로 처리해야 되는 경우도 많을거라 생각이 든 학습이였습니다.
아직 Swift Concurrency에 대해 모르는게 너무 많고 겉만 알고 있던것 같네요🤔
[참고 자료]
https://www.swiftbysundell.com/articles/swift-concurrency-multiple-tasks-in-parallel/
'Concurrency' 카테고리의 다른 글
Swift Concurrency - Async sequence & stream (9) 2023.03.09 Swift Concurrency - map & forEach (14) 2023.03.09 Swift Concurrency - Task (4) (8) 2023.03.02 Swift Concurrency - Task (3) (4) 2023.03.02 Swift Concurrency - Task (2) (2) 2023.02.27