일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- iSP
- 책임주도설계
- 믹스인
- 추상화
- 메서드를 통한 해결
- 객체지향
- 행동 호환성
- 런타임 의존성
- Swift#flatMap#map#Monad#함수형 프로그래밍#Optional
- OCP
- 명령-쿼리 분리
- 합성
- 일관성 있는 협력
- dip
- 유연한 설계
- 유여난 설계
- 상속
- 다형성
- 하향식 접근
- 객체 생성 사용 분리
- 컴파일 타임 의존성
- 알고리즘
- 의존성
- OOP
- '기존 설계 재사용
- Apple # HIG #iOS15 #iOS14 #Human #Interface #Guidelines #Apple developer # Apple human interface guidelines
- 오브젝트
- 서브 타이핑
- 상속 조합 폭발적 증가
- 설계 재사용
- Today
- Total
도니의 iOS 프로그래밍 세상
Group Task - Swift Concurrency(4) 본문
지난 시간에는 async let을 사용하여, concurrent하게 이미지를 다운로드 받는 방법에 대해 알아보았습니다.
이번시간에는 전부 async let을 처리하는것이 아닌, group Task라는 기능을 이용하여 작업을 병렬적으로 실행하는 방법에 대해서 이야기하겠습니다.
지난 포스팅을 보지 않으셨다면, async let(3)를 보고 오시면 이해가 더욱 쉽습니다.
Group Task가 필요한 이유
async let은 비교적 간단하게 병렬적으로 이미지를 다운받을 수 있는 기능입니다.
하지만, async let의 한계는 static하게 결정된 task에서만 사용이 가능하다는 것입니다.
위 코드에서 보듯, 분명하게 두개의 child task가 생성될 거라는 점이 컴파일 시점에서도 이를 명확하게 알 수 있습니다.
for문 내에서 호출된 fetchOneThumbnail이라는 함수는 두개의 child task가 생성될 거라는 점을 알수 있다는 거죠. 하지만 concurrency의 양은 변하지 않습니다. 왜냐하면 두개의 child task는 다음 for-loop 반복전에 끝나야 한다는 것입니다. for문 내에 두개의 child task만 생성되는 상태라는 점입니다.
하지만, 우리는 단순히 이미지를 하나씩 불러오는게 아닌, 모든 thumbnail을 concurrent하게 불러오는게 목표입니다. 그때 우리는 필요한 concurrency의 개수를 알수 있을까요?
정답은 “불가능하다”입니다.
array내부 element개수가 몇개가 될지 모르는 상황이기 때문이죠.
오로지 dynamic하기 때문에 이는 런타임때어야 알 수 있고, 런타임때마다 달라지는 for loop내부 구문의 모든 child task 생성하기 위해선 Task group을 이용해야 합니다.
Task group
Task group은 Structured concurrency에서 dynamic concurrency를 제공하기 위해 만들어졌습니다. 결국 런타임때마다 달라질 수 있는 concurrency를 동적으로 제공하도록 설계 되었다는 겁니다.
(쉽게 말해 for문 내에서 몇개의 child task가 만들어질지 모르는 상황에 대응할 수 있는 기능입니다.
1. withThrowingTaskGroup 사용
우리는 withThrowingTaskGroup이라는 기능을 이용해, Group Task를 생성합니다. 이제 group task내부에서는 dynamic한 개수를 가진 task를 생성하는게 가능합니다.
따라서 for문안에 있는 구문들이 순서대로 동작하지 않고 “병렬적”으로 실행됩니다.
2. group addTask기능 이용
(error의 이유는 나중에 설명드릴테니 일단 addTask를 하는 메커니즘을 알려드릴게요!)
group의 addTask를 통해서 group 내에 child task가 만들어집니다.
일단 group에 넣게되면 child task는 즉각적으로 어떤 순서로든 실행되게 됩니다. group 블록 구문을 벗어나기 위해선 모든 task가 종료되어야 합니다.
결과적으로 우리는 모든 이미지들을 동시에 다운로드해서 불러오는게 가능해졌습니다.
group.addTask내부에서 호출한 fetchOneThumbnail함수에는 두개의 async let구문이 존재합니다.
우리는 group Task로 만들어진 task아래에 child task로서 async let task를 만드는게 가능합니다.
결국 우린 group task안에 async let을 사용할수도 있고, async let task와 함께 group Task를 만들수 있게 되었습니다.
하지만..
아까봤던 에러가 마음에 걸립니다. 무슨 에러일까요?
3. Data Race Condition
사실, 아까처럼 코드를 구현하게 되면 컴파일러는 다음과 같은 Data Race에 관한 경고메세지를 날립니다.
우리는 사실 새로운 Task를 만들때마다 Sendable 클로저라는 새롭게 등장한 클로저 내에서 task가 작업을 수행합니다. 이때 Task가 실행하고 난 뒤, 해당 값들이 수정될 수 있기 때문에, Sendable 클로저는 변할수 있는 값의 캡처를 제한합니다.(이런 이유는, 여러 child task에서 접근하며 값을 변경하다 보면 race condition이 발생할 수 있는 구조이기 때문입니다. 다중 쓰레드에서 한가지 “변할 수 있는” 값에 접근한다면 반드시 Data race 상황이 따라오기 마련입니다.)
Task 생성은 Sendable closure내부에서 일어나고, 해당 클로저는 변할 수 있는 변수들을 capturing하지 않습니다.
다만, value type이나 actor, 그리고 자기자신만의 동기화를 가진 class라면 capture를 허용합니다.
결과적으로 Data race를 방지할 수 있는 조건에서만 Task내부에서 값을 변경하는게 가능하다는 겁니다.
이제 Data race를 막기 위해서 각각의 child Task는 값을 변경하지 않고, data를 return하고 있습니다.
이런 설계구조로 인해, parent task(상위 task)에서만 유일하게 결과 처리를 담당하게 됩니다. child task는 String id와 image를 리턴만 하도록 변경하고 parent task가 child task에서 리턴된 튜플을 처리하게 됩니다.
parent task는 child task로부터의 결과를 반복하기 위해서 새로운 for-await loop를 사용합니다. 해당 await-loop를 통해 child task의 결과를 가져오게 되는 것입니다. 결과적으로 해당 루프는 sequential하게 실행되기 때문에 안전하게 tuple 값의 쌍들을 가져오게 됩니다.
'Swift' 카테고리의 다른 글
[Swift] flatMap, map의 차이 - Monad로의 여정(1) (0) | 2023.12.15 |
---|---|
Async let - Swift Concurrency(3) (0) | 2023.03.09 |
Swift Error handling(try, catch) - Swift Concurrency(2) (0) | 2023.02.09 |
Swift Concurrency(1) - 기본개념 및 작동원리 (0) | 2023.02.08 |
[Swift] ARC weak self를 하는 이유(delayed deinitialization, memory leak) - ARC 응용편 5탄 (0) | 2023.01.15 |