├── README.md ├── Week1 └── ProcessThread.md ├── Week2 ├── DataRace.md └── 동시&직렬, 동기&비동기.md ├── Week3 └── GCD.md ├── Week4 └── swift concurrency.md ├── Week5 └── Actor, Sendable.md └── Week6 ├── Copyable, NonCopyable.md └── Images ├── 38a38e1c-e402-4403-97e3-4eb3dbbb61a3.png ├── image 1.png ├── image 10.png ├── image 11.png ├── image 12.png ├── image 2.png ├── image 3.png ├── image 4.png ├── image 5.png ├── image 6.png ├── image 7.png ├── image 8.png ├── image 9.png ├── image.png ├── 스크린샷_2025-02-06_19.17.28.png ├── 스크린샷_2025-02-06_19.19.10.png ├── 스크린샷_2025-02-06_19.23.38.png └── 스크린샷_2025-02-06_19.25.47.png /README.md: -------------------------------------------------------------------------------- 1 | # Swift-Concurrency-Study 2 | > Swift 6.0을 위한 동시성 스터디 3 | 4 | ### 스터디 목표 5 | - Swift 6.0 동시성을 학습 6 | - 학습한 내용을 기반으로 `기록소` 프로젝트 코드 품질 증가 7 | 8 |
9 | 10 | ## 📚 Swift Concurrency 커리큘럼 11 | 12 | | 주차 | 학습 주제 | 활동 | 13 | |------|---------|---| 14 | | **Week 1** | Process & Thread 개념 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week1/ProcessThread.md) | 15 | | **Week 2** | 공유자원과 임계영역 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week2/DataRace.md) | 16 | | | 직렬-동시, 동기-비동기 개념 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week2/%EB%8F%99%EC%8B%9C%26%EC%A7%81%EB%A0%AC%2C%20%EB%8F%99%EA%B8%B0%26%EB%B9%84%EB%8F%99%EA%B8%B0.md) | 17 | | **Week 3** | 동시성 프로그래밍 with GCD | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week3/GCD.md) | 18 | | **Week 4** | Swift Concurrency 등장 배경 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week4/swift%20concurrency.md) | 19 | | | 비동기 호출에서의 스레드 제어권 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week4/swift%20concurrency.md#%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%A0%9C%EC%96%B4%EA%B6%8C) | 20 | | | Task와 구조화된 동시성(= Structured Concurrency) | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week4/swift%20concurrency.md#async-let) | 21 | | **Week 5** | Actor 개념 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week5/Actor%2C%20Sendable.md#%EB%AA%A9%EC%B0%A8) | 22 | | | Sendable 프로토콜 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week5/Actor%2C%20Sendable.md#sendable) | 23 | | | Main Actor 개념 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week5/Actor%2C%20Sendable.md#global-actor) | 24 | | **Week 6** | 얕은 복사 & 깊은 복사 (+ 클래스에서의 깊은 복사) | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week6/Copyable%2C%20NonCopyable.md#%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC) | 25 | | | Copyable 프로토콜과 ~Copyable | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week6/Copyable%2C%20NonCopyable.md#copyable) | 26 | | | Generic과 Extension에서의 활용 | [활동 링크](https://github.com/Swift-Concurrency-Study/Study/blob/main/Week6/Copyable%2C%20NonCopyable.md#%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EA%B3%BC-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%99%9C%EC%9A%A9) | 27 | | **Week 7** | 기록소 프로젝트 코드 리팩토링 - 1편 | [활동 링크](https://github.com/boostcampwm-2024/iOS10-MemorialHouse/pull/167) | 28 | | **Week 8** | 기록소 프로젝트 코드 리팩토링 - 2편 | [활동 링크](https://github.com/boostcampwm-2024/iOS10-MemorialHouse/pull/176) | 29 | 30 |
31 | 32 | ## 👨🏻‍💻 스터디 방식 33 | 34 | - **날짜**: 매주 금요일 9시 (+- 1시간) 35 | - **진행 방식**: 36 | 1. 스터디를 위해 조직/레포 생성 37 | 2. 매주 각자 주제를 학습하고 노션에 정리 38 | 3. 해당 주제와 관련된 면접 질문도 작성 39 | 4. 스터디 날짜에 랜덤으로 2명 선정: 40 | - 1명: 발표 담당 41 | - 1명: 정리 담당 42 | 5. 정리 담당자는 4명의 정리 내용을 취합해 최종본을 레포에 업로드 43 | -------------------------------------------------------------------------------- /Week1/ProcessThread.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | 3 | - [운영체제는 왜 등장했을까?](#운영체제는-왜-등장했을까) 4 | - [System Call](#system-call) 5 | - [Process](#process) 6 | - [Process 생성과정](#process-생성-과정) 7 | - [Process LifeCycle](#process-lifecycle) 8 | - [PCB](#pcb) 9 | - [Context Switch](#context-switch) 10 | - [Thread](#thread) 11 | - [Process, Thread Memory](#process-thread-memory) 12 | - [multi tasking, multi processing](#multi-tasking-multi-processing) 13 | 14 | # 운영체제는 왜 등장했을까? 15 | 16 | ‘이렇게 비싼 컴퓨터를 어떻게 효율적으로 활용할까?’, ‘컴퓨터가 입출력하는 동안 다른 프로그램을 실행하면 좋지 않을까?’ 라는 생각에 컴퓨터를 운영해주는 프로그램인 운영체제를 떠올렸다. 17 | 18 | 즉, 운영체제는 컴퓨터 운영의 효율성, 편리성 향상을 위해 등장했다. 19 | 20 | # System Call 21 | 22 | 운영체제는 프로그램이며 입/출력 장치 등 하드웨어를 관리한다. 23 | 24 | 우리가 하드웨어에 직접 명령하면 운영체제와 충돌이 날 수 있기 때문에 운영체제에 명령해야한다. 25 | 26 | 아무나 운영체제에 명령할 수 없기에 적절한 권한을 얻어야하는데 이를 `System Call`이라고 한다. 27 | 28 | 프로세스의 권한이 user mode → kernel mode로 넘어가게 된다. 29 | 30 | user mode: 사용자 프로그램들이 수행 (권한↓) 31 | 32 | kernel mode: 운영체제의 커널 수행 (권한↑) 33 | 34 | # Process 35 | 36 | 운영체제는 프로그램의 실행을 돕기 위해 등장했고 정말 중요한 건 프로그램의 실행이다. 37 | 38 | 프로세스는 **실행중**인 프로그램이다. 39 | 40 | → **실행중**이란, 운영체제에 의해 메모리, CPU 등 시스템 자원을 할당받아 작업을 수행하는 상태이다. 41 | 42 | 또한, 프로세스는 포그라운드 프로세스와 백그라운드 프로세스로 나뉜다. 43 | 44 | > **프로그램이란?** 45 | > 어떤 문제를 해결하기 위하여 그 처리 방법과 순서를 기술하여 컴퓨터에 주어지는 일련의 명령문 집합체. 46 | 47 | 그럼, 우리의 컴퓨터에서 프로세스가 어떤 형태로 존재하는지 확인해보자. 48 | 49 | 아래는 시스템의 자원 할당에 대한 사진이다. 50 | 51 | image 52 | 53 | 프로세스가 598개 존재함을 확인할 수 있는데, 프로세스들은 CPU 자원을 사이좋게 나눠 사용하고 있다. 54 | 55 | image 1 56 | 57 | 그렇다면, 이렇게 수 많은 프로세스를 어떻게 적은 양의 CPU 코어로 관리할까?(내 컴퓨터의 코어는 8 Core이다.) 58 | 59 | 프로세스의 상태를 나누어 많은 프로세스들을 관리하게 되는데, 프로세스의 생성과정과 라이프사이클에 대해 알아보자 60 | 61 | # Process 생성 과정 62 | 63 | 1. 프로그램 코드를 메모리로 적재한다. - Load 64 | - 게으른 방식으로 필요할 때마다 가져옴 65 | 2. 실행시간 스택 할당 66 | 3. 힙 생성 67 | - 실행 도중 동적으로 요구되는 공간 할당을 위한 영역 68 | 4. 입출력 초기화 작업 69 | - stdin, stdout, stderr 장치와 연결 설정 70 | - I/O 또는 시그널 관련 자료구조 초기화 71 | 5. 프로그램 시작점으로 분기하여 실행 시작 72 | - 주로 main() 73 | 74 | # Process LifeCycle 75 | 76 | ![image 2](https://github.com/user-attachments/assets/6563fa01-c916-4666-a7b7-1d7aef6a00d2) 77 | 78 | New는 시작, Exit는 종료이다. 나머지 세 가지 상태를 알아보자. 79 | 80 | Ready: 프로세스가 Running에 대한 준비 상태. 메모리에 올라간 상태다. CPU 자원 할당 X 81 | 82 | Running: 프로세스가 메모리에 올라가 CPU 자원을 할당 받은 상태다. 83 | 84 | Blocked: 대기 상태인데, 이 때는 프로세스가 메모리에서 내려와 Disk로 이동하기도 한다. 85 | 86 | # PCB 87 | 88 | 그럼 프로세스의 상태에 대한 정보, 처리는 어떻게 할까? 89 | 90 | Process Control Block(PCB)을 활용 91 | 92 | 프로세스가 생성될 때 운영체제는 PCB를 생성 93 | 94 | ![image 3](https://github.com/user-attachments/assets/4ad3d326-ea67-45a9-9754-8d6759dc6775) 95 | 96 | # Context Switch 97 | 98 | 그렇다면 이제 실제 프로세스 간의 CPU 바톤 터치를 할때는 어떻게 할까? 99 | 100 | - PC, 레지스터 등 program context 저장 101 | - 프로세스의 제어블록 갱신 102 | - 프로세스 제어블록 → 적절한 큐로 옮김 103 | - 다음에 수행할 프로세스 선택 104 | - 선택한 프로세스의 제어블록 갱신(to running) 105 | - 메모리 관리와 관련된 자료구조 갱신 106 | - 선택한 프로세스가 사용하던 context를 복원 107 | 108 | 이러한 절차를 `Context Switch`라고 한다. 109 | 110 | 프로세스, 스레드 모두 컨텍스트 스위칭이 가능하다. 111 | 112 | 프로세스는 PCB, 스레드는 TCB를 사용해 Context가 관리된다. 113 | 114 | 그렇다면 이런 프로세스 교환시점은 언제 발생할까? 115 | 116 | `운영체제가 프로세스로부터 제어를 받을 때`이다. 117 | 118 | - 운영체제가 프로세스로부터 제어를 받을 때 119 | - 인터럽트: 現 명령어 외부 (비동기 이벤트 등) 120 | - 클럭 인터럽트: 시간 다 됐을 때 121 | - 입출력 인터럽트 122 | - 메모리 폴트 123 | - 트랩: 現 명령어 수행 관련 (오류, 예외 등) 124 | - Supervisor 호출: 명시적 요청 (운영체제 기능 호출 등) 125 | 126 | 여기서 알아야 할 것은 현재 Running상태인 프로세스가 멈추고 운영체제가 업무를 보게 된다는 것이다. 127 | 128 | 우리의 목표가 무엇인지 다시 상기해보자. ⇒ 우리의 목표: 프로그램을 실행하는 것 129 | 130 | 즉, Context Switch를 하는 순간 우리는 손해를 보게 된다. 131 | 132 | 그래서 이것을 최소화하는 방향으로 스케쥴링 알고리즘과 멀티 스레딩 등 여러 기법들이 필요해진다. 133 | 134 | # Thread 135 | 136 | 프로세스가 운영체제로부터 자원을 할당받고 스케쥴링된다면, 137 | 스레드는 프로세스에 의해 관리되고 스케쥴링된다. (물론 이것도 운영체제 정책에 따라 달라질 수 있음) 138 | 139 | **스레드는 왜 필요해졌을까?** 140 | 141 | 위에서 확인했듯이 `Context Switch`는 비용이 크다. 142 | 143 | 이러한 부분을 개선하기 위해 스레드가 등장한다. 144 | 145 | - 스레드 생성 시간이 프로세스 생성 시간보다 짧다. 146 | - 스레드 종료 시간이 프로세스 종료 시간보다 짧다. 147 | - 스레드 교환 오버헤드가 프로세스 교환 오버헤드보다 적다. 148 | - 스레드 간 통신 오버헤드가 프로세스 간 통신 오버헤드보다 적다. 149 | 150 | 그렇다면 왜 스레드가 프로세스보다 더 빠를까? 메모리 구조를 통해 확인해보자. 151 | 152 | # Process, Thread Memory 153 | 154 | ![image 4](https://github.com/user-attachments/assets/b5491b62-a15c-47b8-90d1-df2a4a8c91bf) 155 | 156 | 위 사진에서 알 수 있듯이 프로세스는 각각의 자원이 독립적이다. 157 | 158 | 반면, 스레드는 스택영역을 제외한 부분을 공유한다. 159 | 160 | 스레드는 메모리를 공유하고 프로세스는 메모리를 공유하지 않는데, 프로세스 끼리 정보를 주고 받을 수 있을까? 161 | 162 | IPC, LPC, Shared Memory 등을 활용해서 정보를 주고 받을 수 있다. 163 | 164 | 이러한 이유로 메모리를 공유하는 스레드에 비해 프로세스의 속도는 느리다. 165 | 166 |
167 | 168 | **왜 Stack 영역만 독립되어있나??** 169 | 170 | Stack은 다들 알다싶이 함수 호출 시 전달되는 인자, 되돌아갈 주소값, 함수 냉서 선언하는 변수 등을 저장하는 메모리 공간이다. 171 | 172 | 이러한 Stack을 독립되게 가졌다는 것은 독립적인 함수 호출이 가능하다 라는 의미이다. 그리고 독립적인 함수 호출이 가능하다는 것은 곧, 독립적인 실행 흐름이 가능하다는 것이다. 173 | 174 | 즉, Stack 영역을 독립적으로 가짐으로써 스레드는 독립적인 실행 흐름을 가질 수 있게 되는 것이다. 175 | 176 | ![image 5](https://github.com/user-attachments/assets/ecec243d-c634-4988-b07d-fd1da270db51) 177 | 178 | 위 사진은 프로세스의 메모리구조다. 179 | 180 | 스택과 힙 영역은 동적할당되고 데이터와 코드 영역은 정적할당된다. 181 | 182 | 코드(텍스트) 영역: 코드가 기계어 형태로 저장 183 | 184 | 데이터 영역: 전역 변수 저장 185 | 186 | 스택 영역: 지역 변수, 매개 변수 등 저장 → 반복적으로 호출될 경우 stack overflow 발생 187 | 188 | 힙 영역: 주소값을 통해 힙 영역의 데이터에 접근하고 크기는 런타임에 결정. ARC에 의해 관리 189 | 190 | # multi tasking, multi processing 191 | 192 | multi tasking: 단일 Core에서 번갈아가며 여러 작업을 처리하는 것 193 | 194 | ![image 6](https://github.com/user-attachments/assets/d474eb2b-c627-4fb7-8f09-587a781e242f) 195 | 196 | multi processing: 여러개의 Core가 여러 작업을 병렬로 처리하는 것 197 | 198 | ![image 7](https://github.com/user-attachments/assets/36644282-c4c6-4794-932e-285e5a434b64) 199 | 200 | multi tasking + multi processing 201 | 202 | ![image 8](https://github.com/user-attachments/assets/d9a75265-4527-42d6-83a3-b0afd5f18fbf) 203 | 204 | # QNA 205 | 206 | - 프로세스와 스레드의 차이점은 무엇이며, 각각의 메모리 구조는 어떻게 다른가요? 207 | 208 | - 컨텍스트 스위칭(Context Switching)이란 무엇이며, 어떤 상황에서 발생하나요? 209 | - 스택(Stack)과 힙(Heap)의 차이점은 무엇이며, 각각의 메모리 할당 방식은 어떻게 다른가요? 210 | - 프로그램과 프로세스의 차이에 대해 자원(CPU, Memory, Disk)의 관점에서 이야기해보세요 211 | - 스레드가 무엇이고 왜 생겼는지 말해보세요. 212 | - 멀티프로세스와 멀티스레드의 장, 단점에 대해 설명해주세요. 213 | - 프로세스가 무엇인가? - PCB는 왜 필요한가요. 214 | - 스레드와 프로세스의 장점/단점 비교 215 | - 프로세스와 스레드의 차이를 메모리 관점에서 설명해주세요 216 | - 멀티 태스킹과 멀티 프로세싱의 차이점을 설명해주세요 217 | - iOS에서 어떻게 멀티 스레드를 구현할 수 있는지 설명해주세요 218 | - Thread 사용하면서 불편했던 점이 있었나요? 219 | - Thread 클래스가 아닌 GCD를 사용하는 이유는 무엇인가요? 220 | - 스레드가 메모리를 공유함으로써 일어날 수 있는 문제점? 221 | -------------------------------------------------------------------------------- /Week2/DataRace.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | - [경쟁 상태 (Race Condition)](#경쟁-상태-race-condition) 3 | - [상호 배제 (Mutual Exclusion)](#상호-배제-mutual-exclusion) 4 | - [Busy Waiting](#busy-waiting) 5 | - [Sleep and Wakeup](#sleep-and-wakeup) 6 | - [뮤텍스(Mutex)](#뮤텍스-mutex) 7 | - [세마포어(Semaphore)](#세마포어-semaphores) 8 | - [뮤텍스 vs 세마포어](#뮤텍스-vs-세마포어) 9 | - [교착상태 (Dead Lock)](#교착상태-dead-lock) 10 | - [교착상태 조건](#교착상태-조건) 11 | - [교착상태 해결책](#교착상태-해결책) 12 | - [Data Race](#data-race) 13 | - [Data Race가 위험한 이유?](#data-race가-위험한-이유) 14 | - [iOS에서 Data Race 해결책](#ios에서-data-race-해결책) 15 | - [QnA](#qna) 16 | 17 | # 경쟁 상태 (Race Condition) 18 | 19 | **경쟁 상태(Race Condition)** 란 두 개 이상의 흐름에서 어떤 기능의 실행되는 시점, 순서에 따라 공유자원의 상태(결과)가 달라지는 상태를 의미한다. 20 | 21 | 예를 들어 아래와 같은 코드가 있다고 하자. 22 | 23 | ```swift 24 | bool a = NULL; 25 | Thread1.do { a = true; } 26 | Thread2.do { a = false; } 27 | ``` 28 | 29 | 위 코드에서 Thread1과 Thread2는 거의 동시에 동작한다. 30 | 31 | 이를 계속해서 실행하면 a의 상태가 언제는 true, 언제는 false로 달라지는 것을 확인할 수 있다. 32 | 33 | 이것은 프로그램을 사용하는 입장에서 예측 가능한 결과를 보장해주지 않는다. 34 | 35 | 이렇게 Race Condition이 발생하는 코드를 **임계 구역 (Criticial Section)**이라고 한다. 36 | 37 | 이러한 임계 구역이 나타나는 이유는 코드를 실행할 때 원자적으로 수행되지 않고 실제로는 아래와 같이 3단계의 명령어로 나뉘어 실행되기 때문이다. 38 | 39 | 40 | 41 | ## 상호 배제 (Mutual Exclusion) 42 | 43 | 위에서 언급한 임계 구역을 해결하기 위해서는 어떻게 해야할까? 44 | 45 | 간단하게 생각하면 먼저 접근하는 쪽에서 접근 중이라는 표시를 해주고 뒤에 들어온 작업은 먼저 들어온 작업이 끝날 때까지 기다리면 된다. 46 | 47 | 이렇게 하나의 스레드 혹은 프로세스만 임계 구역을 실행하도록 보장하는 기법을 **상호 배제(Mutual Exclusion)** 라고한다. 48 | 49 | 상호 배제는 아래 규칙을 지켜야 성공적인 솔루션을 제공한다고 볼 수 있다. 50 | 51 | 1. 임계 구역에 하나의 프로세스 혹은 스레드만 진입해야 한다. 52 | 2. 프로세스 혹은 스레드는 진입을 위해 영원히 대기해서는 안된다. → 교착상태 (DeadLock) 53 | 3. 임계 구역 외부의 프로세스 혹은 스레드가 다른 프로세스 혹은 스레드를 막아서는 안된다. 54 | 4. CPU의 성능과는 독립적이어야 한다. 55 | 56 | 그럼 상호 배제 기법들을 알아보자. 57 | 58 | ### Busy Waiting 59 | 60 | > 임계 구역을 다른 프로세스 혹은 스레드가 사용 중인지 여부를 while문을 통해 지속적으로 확인하는 방법 61 | > 62 | 63 | 확실하긴 하지만, 할당받은 CPU 사용시간을 낭비한다는 단점이 있다. (지양해야댐..) 64 | 65 | Busy Waiting의 알고리즘은 아래 3가지가 존재한다. 66 | 67 | 1. Strict Alternation 68 | 69 | 70 | 71 | 해당 알고리즘은 임계 구역에 2개의 스레드가 들어갈 수 있다. 또한, 임계 구역 외부 while문에서 block되고 있으므로 3번 규칙을 어기게된다. 72 | 73 | 2. Peterson’s solution 74 | 75 | 76 | 77 | 다른 프로세스가 임계구역에 접근하는지, 누구의 차례인지를 검사하여 임계 구역에 들여보내는 방식이다. 78 | 79 | 3. TSL instruction 80 | 81 | 82 | 83 | 해당 방식은 TSL 레지스터, LOCK(Test and Set Lock)이라는 어셈블리 명령어를 사용한다. 84 | 85 | 프로그램 수준이 아닌 어셈블리 수준에서 상호배제를 보장하는 것이다. 86 | 87 | 락을 설정하는 데에 있어 인터럽트를 할 수 없게 한다는 장점이 있다. 88 | 89 | 90 | 이렇게 많은 알고리즘들이 있지만 결국 busy waiting의 한계(기다리는 시간 낭비)를 벗어날 순 없다. 91 | 92 | ### Sleep and Wakeup 93 | 94 | Sleep and WakeUp은 System call을 통해서 sleep을 호출한 프로세스 혹은 스레드를 재우고, 다른 프로세스가 wakeup을 해주는 구조이다. 95 | 96 | **생산자 - 소비자(Producer - Consumer)문제**는 Sleep and Wakeup과 함께 알아두면 좋은 문제이다. 우리가 데이터를 교환하는 것을 보여 누구는 데이터를 만들고 누구는 그 데이터를 사용한다는 점을 알 수 있다. 이를 생산자(Producer)와 소비자(Consumer)에 빗대어 표현한 것이다. 97 | 98 | 데이터를 무한히 버퍼에 저장할 순 없다. 즉, 언젠가는 생산/소비를 멈춰야한다. 99 | 100 | 여기서 Sleep and Wakeup 개념이 사용된다. 101 | 102 | 생산자는 버퍼가 꽉차면 sleep 후 소비자를 깨우고, 소비자는 버퍼가 비면 sleep 후 생산자를 깨우는 방식이다. 103 | 104 | 하지만 이 방식의 경우 잘못하면 모두가 자버릴 수 있다. 105 | 106 | 107 | 108 | 위 코드에서 count가 N일 때 생산자가 if (count == N)분기까지 하고 interrupt되고 소비자가 wakeup 코드를 실행해버리면 생산자는 영원한 잠에 빠진다. 109 | 110 | 이처럼 다른 프로세스 혹은 스레드가 영원히 잠에 빠질 수 있는 것이다. 111 | 112 | ### 뮤텍스 (Mutex) 113 | 114 | 프로세스나 스레드가 공유자원을 `Lock()` 으로 잠금 설정하고 Lock 소유권을 얻어 임계 구역에서 작업하는 방법을 말한다. 115 | 116 | 117 | 118 | 작업이 끝난 후 해당 프로세스 혹은 스레드가 `Unlock()`을 통해 잠금을 해제하고 다른 프로세스 혹은 스레드가 접근할 수 있게 된다. 119 | 120 | 잠금이 설정되어있는 동안은 공유자원에 다른 프로세스나 스레드는 접근할 수 없다. 121 | 122 | Lock을 기다리는 프로세스 혹은 스레드들은 Busy Waiting에 빠지게 되는데, 이 때 Sleep을 하여 대기 큐에 넣고, 사용 가능할 때 프로세스 혹은 스레드를 깨운다. 123 | 124 | ### 세마포어 (Semaphores) 125 | 126 | 일반화된 뮤텍스로, 간단한 정수 값과 `wait`, `signal`로 공유 자원에 대한 접근을 처리한다. 127 | 128 | 129 | 130 | 프로세스나 스레드가 공유 자원에 접근하면 세마포어에서 `wait()` , 접근이 끝나면 `signal()` 을 수행한다. 131 | 132 | 세마포어에는 조건 변수가 없고, 세마포어 값 수정 시 다른 프로세스는 동시 수정이 불가능하다. 133 | 134 | ### 뮤텍스 vs 세마포어 135 | 136 | 뮤텍스는 세마포어에서 접근 가능 개수를 2개로 설정한 것과 비슷해 보이는데 어떤 차이가 있을까? 137 | 138 | 가장 중요한 차이점은 **공유 자원의 Lock에 대한 소유권**이다. 139 | 140 | 일반적인 세마포어의 경우 Lock에 대한 소유권이 없다. 따라서 다른 프로세스 혹은 스레드가 signal 등을 통해 변수를 조작하여 임계 구역에 대한 접근 권한을 획득할 수 있다는 잠재적인 위험요소가 존재한다. 141 | 142 | 하지만 뮤텍스의 경우 해당 임계 구역에 진입한 프로세스 혹은 스레드가 Lock에 대한 소유권을 갖는다. 따라서 Unlock을 하는 것도 진입한 프로세스 혹은 스레드만이 가능하다. 143 | 144 | ## 교착상태 (Dead Lock) 145 | 146 | **교착상태 (Dead Lock)** 은 두 개 이상의 프로세스 혹은 스레드가 동시에 공유자원에 접근하려다 잠겨서 서로 아무것도 하지 않는 상태를 의미한다. 147 | 148 | 149 | 150 | ### 교착상태 조건 151 | 152 | 1. 상호 배제 (Mutual Exclusion) 153 | 2. 점유 상태로 대기 (Hold and wait): 임계 구역을 점유한 채로 대기 154 | 3. 선점 불가 (No preemption): 다른 프로세스 혹은 스레드에 CPU 양보 안함 155 | 4. 순환성 대기 (Circular wait): 서로가 서로를 기다림 156 | 157 | ### 교착상태 해결책 158 | 159 | 1. 예방 (Prevention) 160 | 161 | 교착상태 조건 중 하나 이상을 부정하면 된다. (확실하지만 자원 소모 심함;;) 162 | 163 | - 상호 배제 부정 : Race Condition을 허용하게 된다. 164 | - 점유 및 대기 부정 165 | - 모든 자원을 미리 할당한다 → 자원 낭비 166 | - 자원 점유를 안 할 때에만 요청을 수락한다 → 기아 상태 167 | - 비선점 부정 168 | - 카메라, 키보드 등 I/O 자원을 공유하는 것도 문제가 될 수 있다. 169 | - 순환선 대기: 번호 달고 대기 → 자원낭비 170 | 2. 회피 (Avoidance) 171 | 172 | 운영체제가 프로세스 혹은 스레드가 교착상태에 빠질 가능성을 계산하는 것이다. 173 | 174 | 주로 은행원 알고리즘이 이곳에 해당한다. 175 | 176 | > 은행원 알고리즘: 총 자원의 양과 현재 할당한 자원의 양을 기준으로 안정, 불안정 상태로 나누고 안정 상태로 가도록 자원을 할당하는 것 177 | > 178 | 3. 탐지 (Detection) 179 | 180 | 현실적으로 모든 상황을 회피할 순 없으니 어디서 교착상태가 발생했는지 알아내는 작업이다. 181 | 182 | 4. 복구 (Recovery) 183 | 184 | 탐지를 하고 나서 교착상태를 해결하려면 프로세스 및 스레드를 종료하거나 자원을 회수해야한다. 185 | 186 | 그런데 교착상태가 발생한 프로세스가 운영체제라면? 우짬요? 응 재부팅해야돼 ~ ㅋㅋ 187 | 188 | 189 | # Data Race 190 | 191 | 휴 ~ 드디어 이번 주차의 주제가 나왔다. 192 | 193 | Data Race는 동기화 기법이 적용되지 않은 상황에서 공유 데이터에 읽기와 쓰기가 동시에 발생하는 경우를 의미한다. 194 | 195 | 위에서 나온 Race Condition과 많이 헷갈려하는데 보통 Race Condition이 조금 더 넓은 범위로 사용된다. 196 | 197 | ## Data Race가 위험한 이유? 198 | 199 | 1. 디버깅이 어렵다 200 | - 테스트 환경에서는 문제가 드러나지 않다가, 실제 환경에서만 발생할 수 있다. 201 | - 항상 동일한 결과를 재현하지 어렵다. 202 | 2. 비결정성 203 | - 같은 코드를 실행해도 결과가 달라져 안정적인 시스템 설계가 어렵다. 204 | 3. 데이터 손상 205 | - 데이터 구조나 상태가 손상되면 프로그램 전체에 예기치 않은 동작을 초래할 수 있다. 206 | 4. 보안 취약점 207 | - 공격자가 Data Race를 악용해 메모리 상태를 의도적으로 조작 및 탈취할 수 있다. 208 | 209 | ## iOS에서 Data Race 해결책 210 | 211 | Data Race를 만들지 않기 위해 예방하는 방법에는 크게 4가지 정도가 존재한다. 212 | 213 | - NSLock 214 | - `NSLock` 이라는 클래스의 객체를 만들어두고 `.lock()`, `.unlock()` 메서드를 통해 변수를 건드리는 작업을 할 때마다 락을 걸었다 풀었다는 해주는 방법 215 | - `NSLock`을 사용할 때 주의할 사항은 Lock을 걸었던 스레드에서 Lock을 풀어줘야하는 것이다. 216 | - 예시 코드 217 | 218 | ```swift 219 | final class SynchronizedInteger { 220 | private var _value: Int 221 | private let lock = NSLock() 222 | 223 | var value: Int { 224 | get { 225 | lock.lock() 226 | let value = _value 227 | lock.unlock() 228 | return value 229 | } 230 | set { 231 | lock.lock() 232 | _value = newValue 233 | lock.unlock() 234 | } 235 | } 236 | 237 | init(_ value: Int) { 238 | self._value = value 239 | } 240 | 241 | func increment() { 242 | lock.lock() 243 | _value += 1 244 | lock.unlock() 245 | } 246 | } 247 | ``` 248 | 249 | - DispatchQueue barrier 250 | - DispatchQueue에 작업을 넣을 때 flag를 지정함으로써 배리어 블럭처럼 작동하게 하는 것이다. 251 | - 배리어 블럭이 DispatchQueue에 추가되면 큐는 배리어 블럭의 실행을 배리어 블럭 이전에 들어간 모든 작업이 끝날 때까지 미루게 된다. 252 | - 예시 코드 253 | 254 | ```swift 255 | final class BarrierInteger { 256 | private var _value: Int 257 | private let queue = DispatchQueue(label: "BarrierInteger", attributes: .concurrent) 258 | 259 | var value: Int { 260 | get { 261 | var value: Int! 262 | queue.sync { 263 | value = _value 264 | } 265 | return value 266 | } 267 | set { 268 | queue.async(flags: .barrier) { 269 | self._value = newValue 270 | } 271 | } 272 | } 273 | 274 | init(_ value: Int) { 275 | self._value = value 276 | } 277 | 278 | func increment() { 279 | queue.async(flags: .barrier) { 280 | self._value += 1 281 | } 282 | } 283 | } 284 | ``` 285 | 286 | - DispatchSemaphore 287 | - GCD의 일부로 Semaphore가 구현되어있는데 Binary Semaphore(동시에 접근할 수 있는 스레드의 개수가 1개인 세마포어)로 사용할 수 있다. 288 | - `NSLock`과 비슷하지만 세마포어의 특성상 `wait()` 메서드를 통해 락을 건 스레드가 아닌 다른 스레드에서도 `signal()` 메서드를 호출하여 걸려있는 락을 풀 수 있다. 289 | - 예시 코드 290 | 291 | ```swift 292 | final class SemaphoredInteger { 293 | private var _value: Int 294 | // 동시에 접근할 수 있는 스레드는 1개 295 | private let semaphore = DispatchSemaphore(value: 1) 296 | 297 | var value: Int { 298 | get { 299 | semaphore.wait() 300 | let value = _value 301 | semaphore.signal() 302 | return value 303 | } 304 | set { 305 | semaphore.wait() 306 | _value = newValue 307 | semaphore.signal() 308 | } 309 | } 310 | 311 | init(_ value: Int) { 312 | self._value = value 313 | } 314 | 315 | func increment() { 316 | semaphore.wait() 317 | _value += 1 318 | semaphore.signal() 319 | } 320 | } 321 | ``` 322 | 323 | - Actor 324 | - Swift Concurrency에서 등장한 타입으로 actor 타입을 사용하면 actor가 제공하는 동기화 메커니즘을 사용할 수 있다. (~~이건 이제.. 추후 알아볼 예정이니 간단하게만 적어두겠다 ^^~~) 325 | 326 | # QnA 327 | 328 | - Data Race와 Race Condition의 차이를 설명해주세요. 329 | - Swift에서 Data Race를 예방할 수 있는 방법을 설명해주세요. 330 | - 경쟁 조건이 발생하는 이유와 이를 방지하기 위한 방법에는 어떤 것들이 있을까요? 331 | - 뮤텍스와 세마포어의 차이점을 설명해주세요. 332 | - 교착상태(데드락)가 발생하는 조건을 설명하고 이를 해결하는 방법에는 어떤 것들이 있는지 설명해주세요. 333 | - 상호 배제는 Race Condition과 Data Race를 해결할 수 있나요? 334 | - 교착상태는 무엇이며 조건이 무엇인가요? -------------------------------------------------------------------------------- /Week2/동시&직렬, 동기&비동기.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | 3 | - [동시(Concurrent) vs 직렬(Serial)](#동시concurrent-vs-직렬serial) 4 | - [동기(Sync) vs 비동기(Async)](#동기sycn-vs-비동기async) 5 | - [병렬 vs 동시](#병렬-vs-동시) 6 | - [블로킹 vs 논블로킹](#블로킹-vs-논블로킹) 7 | - [블로킹과 동기화의 가능한 조합](#블로킹과-동기화의-가능한-조합) 8 | - [실험🧪](#실험-) 9 | - [Serial Queue에서 비동기로 작업하면?](#serial-queue에서-비동기로-작업하면) 10 | - [Concurrent Queue에서 비동기로 작업하면?](#concurrent-queue에서-비동기로-작업하면) 11 | - [QnA](#qna) 12 | 13 | 14 | # 동시(Concurrent) vs 직렬(Serial) 15 | 16 | - 동시(Concurrent): **여러 스레드로** 작업을 분산시켜 보낸다. 17 | 18 | 19 | 20 | - 직렬(Serial): **하나의 스레드로**만 작업들을 보낸다. (작업 순서가 중요할 경우) 21 | 22 | 23 | 24 | # 동기(Sycn) vs 비동기(Async) 25 | 26 | - 동기(Synchronous): 다른 스레드로 보낸 작업이 **끝날때까지 기다린다**. 27 | - 작업이 순차적으로 진행되며 작업을 대기하는 동안 CPU가 낭비된다는 특징이 있다. 28 | - 비동기(Asynchronous): 작업을 다른 스레드에서 시키고, **바로 내 할 일을 한다**. 29 | - 작업 흐름이 복잡해질 수 있지만 다음 작업에 대한 대기 시간이 감소한다는 특징이 있다. 30 | 31 | # **병렬 vs 동시** 32 | 33 |

34 | 35 |

36 | 37 | - 병렬(Parallel): 멀티 코어에서 멀티 스레드를 사용해 **실제로(물리적으로) 여러 작업을 한 번에 실행**시킨다. 38 | - 동시(Concurrent): 싱글 코어에서 멀티 스레드를 사용하여 **논리적으로 여러 작업이 한 번에 실행되는 것처럼 보이게** 한다. 39 | 40 | # 블로킹 vs 논블로킹 41 | 42 | - 블로킹(Blocking): 작업 요청 후, 요청한 코드(caller)의 실행이 멈추고 대기한다. 43 | - 호출된 함수(callee)가 완료될 때까지 점유되며 동기 작업에서 주로 발생한다. 44 | - 기다리는 동안 스레드는 유휴상태이다. 45 | - 논블로킹(Non-blocking): 작업 요청 후, 요청한 코드(caller)가 멈추기 않고 계속 실행한다. 46 | - 호출된 함수(callee)가 완료되면 별도의 방식(call-back, 알림 등)으로 결과를 처리하며 주로 비동기 작업에서 발생한다. 47 | - 스레드는 유휴상태가 아니다. 48 | 49 | ## 블로킹과 동기화의 가능한 조합 50 | 51 | 52 | 53 | # 실험 🧪 54 | 55 | ### Serial Queue에서 비동기로 작업하면? 56 | 57 | 58 | 59 | → 순차적으로 실행 60 | 61 | ### Concurrent Queue에서 비동기로 작업하면? 62 | 63 | 64 | 65 | → 당연히 비순차적으로 돌아가겠쥬? 66 | 67 | # QnA 68 | 69 | - 동기와 비동기의 차이점은 무엇이며, 각각의 장단점은 무엇인가요? 70 | - 직렬(Serial) 큐와 동시(Concurrent) 큐의 차이점과 사용 사례를 설명해주세요. 71 | - 직렬 큐에서 비동기 작업을 실행하면 어떤 결과가 발생하나요? 72 | - 동시성 프로그래밍과 비동기 프로그래밍의 차이를 설명해주세요. -------------------------------------------------------------------------------- /Week3/GCD.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | 3 | - [GCD(Grand Central Dispatch)](#gcdgrand-central-dispatch) 4 | - [DispatchQueue](#dispatchqueue) 5 | - [SerialQueue](#serialqueue) 6 | - [ConcurrentQueue](#concurrentqueue) 7 | - [QoS (Quality of Service)](#qos-quality-of-service) 8 | - [GCD 사용시 주의 사항 !](#gcd-사용시-주의-사항-) 9 | - [main, global](#main-global) 10 | - [DispatchGroup](#dispatchgroup) 11 | - [DispatchWorkItem](#dispatchworkitem) 12 | - [DispatchWorkItem 기능](#dispatchworkitem-기능) 13 | - [DispatchSemaphore](#dispatchsemaphore) 14 | - [DispatchBarrier](#dispatchbarrier) 15 | - [QNA](#qna) 16 | 17 | # GCD(Grand Central Dispatch) 18 | 19 | 기존에는 개발자가 직접 스레드를 생성하고 작업(task)를 할당했다. 20 | 21 | 스레드를 만들어사용하고 없애는 책임까지 개발자에게 있었기에 효율성과 가용성 측면에서 좋지 않았다. 22 | 23 | ![요런식으로 스레드가 많이 생성될 수 있고 관리가 필요함 (내 컴터에서는 6093개가 한계인듯)](https://hanyo3477.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F35745b71-fac8-4aef-ac72-46ac6d683435%2Ff0cc5538-eae8-4b75-9e08-7fcaec0c8658%2Fimage.png?table=block&id=17e17c78-8267-809b-8738-cfc5885046e5&spaceId=35745b71-fac8-4aef-ac72-46ac6d683435&width=480&userId=&cache=v2) 24 | 25 | 요런식으로 스레드가 많이 생성될 수 있고 관리가 필요함 (내 컴터에서는 6093개가 한계인듯) 26 | 27 | 이를 보완하고자 GCD가 등장했다. 28 | 29 | GCD는 thread pool 패턴에 기반한 작업의 병렬 처리를 구현한 것이다. 30 | 31 | ## DispatchQueue 32 | 33 | DispatchQueue 객체는 이를 실현하는 주된 방법이다. 34 | 35 | DispatchQueue 객체를 생성하고 작업을 할당하면 알아서 스레드를 만들고 작업을 수행한 후 스레드를 지운다. 36 | 37 | ![https://hanyo3477.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F35745b71-fac8-4aef-ac72-46ac6d683435%2Fe1ceabb8-a2a0-42c7-a1d9-1113fd9e40f3%2Fimage.png?table=block&id=17e17c78-8267-80e3-b89e-cb2f1c6eb1b7&spaceId=35745b71-fac8-4aef-ac72-46ac6d683435&width=1060&userId=&cache=v2](https://hanyo3477.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F35745b71-fac8-4aef-ac72-46ac6d683435%2Fe1ceabb8-a2a0-42c7-a1d9-1113fd9e40f3%2Fimage.png?table=block&id=17e17c78-8267-80e3-b89e-cb2f1c6eb1b7&spaceId=35745b71-fac8-4aef-ac72-46ac6d683435&width=1060&userId=&cache=v2) 38 | 39 | Synchronous 하게 실행하는 경우 worker에서 동작하던 DispatchQueue가 해당 작업을 넣은 스레드로 이동해서 작업을 수행한다. 40 | 41 | ![Worker의 점선 작업 == Thread의 보라색 작업](https://hanyo3477.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F35745b71-fac8-4aef-ac72-46ac6d683435%2Fe66d7372-3cdf-4972-8223-17f1f23819a1%2Fimage.png?table=block&id=17e17c78-8267-80fd-9996-f652320631ca&spaceId=35745b71-fac8-4aef-ac72-46ac6d683435&width=1060&userId=&cache=v2) 42 | 43 | Worker의 점선 작업 == Thread의 보라색 작업 44 | 45 | ```swift 46 | // queue를 만들고 47 | let queue = DispatchQueue(label: "example") 48 | 49 | // async로 작업을 진행 50 | queue.async { 51 | let smallImage = image.resize(to: rect) 52 | DispatchQueue.main.async { 53 | imageView.image = smallImage 54 | } 55 | } 56 | ``` 57 | 58 | 이런식으로 비동기 작업을 진행할 수 있다. 59 | 60 | 위의 코드는 큐를 serial하게 만들었을 때다. (default == serial) 61 | 62 | ### SerialQueue 63 | 64 | 기본적으로 DispatchQueue는 일단 Queue다. 65 | 66 | 큐 특성상 FIFO로 동작한다. 67 | 68 | ```swift 69 | let queue = DispatchQueue(label: "com.example.imagetransform") 70 | queue.async { 71 | Logger().log("1") 72 | } 73 | queue.async { 74 | Logger().log("2") 75 | } 76 | queue.async { 77 | Logger().log("3") 78 | } 79 | queue.async { 80 | Logger().log("4") 81 | } 82 | queue.async { 83 | Logger().log("5") 84 | sleep(2) 85 | } 86 | queue.async { 87 | Logger().log("6") 88 | } 89 | queue.async { 90 | Logger().log("7") 91 | } 92 | queue.async { 93 | Logger().log("8") 94 | } 95 | queue.async { 96 | Logger().log("9") 97 | } 98 | queue.async { 99 | Logger().log("10") 100 | } 101 | ``` 102 | 103 | 위와 같이 코드를 실행하면 1부터 10까지 순서대로 출력된다. (중간에 sleep을 호출하더라도) 104 | 105 | ### ConcurrentQueue 106 | 107 | 그럼 작업들을 동시에 실행하는 방법은 무엇일까? 108 | 109 | DispatchQueue에 concurrent 옵션을 주는 것이다. 110 | 111 | ```swift 112 | let queue = DispatchQueue(label: "com.example.imagetransform", attributes: .concurrent) 113 | 114 | queue.async { 115 | Logger().log("1") 116 | } 117 | queue.async { 118 | Logger().log("2") 119 | } 120 | queue.async { 121 | Logger().log("3") 122 | } 123 | queue.async { 124 | Logger().log("4") 125 | } 126 | queue.async { 127 | Logger().log("5") 128 | sleep(2) 129 | } 130 | queue.async { 131 | Logger().log("6") 132 | } 133 | queue.async { 134 | Logger().log("7") 135 | } 136 | queue.async { 137 | Logger().log("8") 138 | } 139 | queue.async { 140 | Logger().log("9") 141 | } 142 | queue.async { 143 | Logger().log("10") 144 | } 145 | ``` 146 | 147 | 위의 코드를 실행하면 1~10의 숫자가 순서와 관계없이 출력된다. 148 | 149 | ### QoS (Quality of Service) 150 | 151 | 큐에 대한 우선순위라고 할 수 있다. 152 | 153 | ```swift 154 | let queue = DispatchQueue(label: "com.example.imagetransform", 155 | qos: .background, attributes: .concurrent) 156 | ``` 157 | 158 | QoS 종류는 다음과 같다. (우선순위가 높은 순) 159 | 160 | 1. `userInteractive` 161 | 2. `userInitiated` 162 | 3. `default` 163 | 4. `utility` 164 | 5. `background` 165 | 6. `unspecified` 166 | 167 | 위의 우선순위가 높은 작업을 먼저 실행하도록 한다. 168 | 169 | 그런데 작업 자체에도 우선순위를 부여할 수 있다. 170 | 171 | ```swift 172 | queue.async(qos: .userInteractive) { 173 | Logger().log("1") 174 | } 175 | ``` 176 | 177 | Priority Inversion을 방지하는 차원에서 큐의 우선순위는 큐의 우선순위와 작업의 우선순위 중 더 높은 우선순위를 따른다. 178 | 179 | `DispatchQueue.global(qos: .background).async(.utility)` 180 | 181 | Queue와 Task 각각 QoS가 다름, 둘 중 높은 걸 따라간다. 182 | 183 | - Task QoS > Queue QoS 184 | Task의 우선순위가 큐보다 더 높은 경우 utility로 우선순위가 상승하게 된다. 185 | - Task QoS < Queue QoS 186 | Queue QoS 따라간다. 187 | 188 | %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202025-01-17%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%201 44 19 189 | 190 | 191 | utility 작업이 먼저 큐에 들어가고, 이후 default 작업들이 들어간다. 192 | 193 | 그러나, utility가 끝날 때까지 queue의 QoS는 utility이다. 194 | 195 | ### GCD 사용시 주의 사항 ! 196 | 197 | GCD를 사용할 때 조심해야할 사항이 몇 가지 있다. 198 | 199 | 1. UI는 메인 스레드에서 처리해야한다. 200 | 201 | 메인 스레드가 UI를 담당하는 것은 iOS에만 국한된 것이 아니라, 모든 OS에 적용되는 사항이다. 202 | 203 | 만약 메인 스레드에서 돌아야하는 코드가 있는데 background 스레드에서 돌고있다면 보라색 경고창이 뜨게된다 ! 204 | 205 | Main Thread Checker라는 친구가 해주는 건데, background 스레드에서 유효하지 않은 동작들을 잡아내는 친구이다. (Thread Sanitizer 과 비슷한 그친구요 ~) 206 | 207 | 2. `sync` 메소드에 대한 주의 사항 3가지 208 | 209 | 2-1. 메인 큐에서 다른 큐로 작업을 보낼 때 `sync`를 사용하면 안된다. 210 | 211 | `sync`로 작업을 보낸다는 것은 “해당 작업이 끝날때까지 기다린다”는 것을 의미한다는 것을 모두가 .ᐟ.ᐟ 알 것이다. 메인 스레드는 UI를 업데이터 해줘야하는데 다른 작업들이 끝날때까지 기다린다..? 이건 곧 작업이 끝날때까지 UI 업데이트가 지연된다는 의미로 화면이 버벅여 보일 것이다. 212 | 213 | 따라서 메인 스레드에서 작업을 넘길 때에는 항상 `async`(비동기)로 작업을 보내도록 하자 ~ 214 | 215 | 2-2. 현재와 같은 큐에 `sync`로 작업을 보내면 안된다. 216 | ![image-8](https://github.com/user-attachments/assets/9d6d3c9d-9f36-41c2-bd21-7b5dce765a77) 217 | 218 | 같은 큐에 동기적으로 작업을 보낸다는 건 위와 같은 코드를 의미한다. 219 | 220 | 하나의 큐에서 사용하는 스레드 객체를 정해져있다. 따라서 같은 큐에 작업을 넣으면 같은 스레드에 해당 작업이 배치될 수 있는데, 해당 스레드가 sync로 인해 멈춰있는 상황이라면 교착상태(Dead Lock)가 발생한다. 221 | 222 | 꼭 ! 발생하는 것은 아니지만 발생할 가능성이 있기 때문에 하지말하는 것 ~ 223 | 224 | 참고로 Global Queue는 QoS에 따라 각각 다른 큐 객체를 생성하므로 교착상태가 발생하지 않음 ~ 225 | 226 | 2-3. 메인 스레드에서 `DispatchQueue.main.sync`를 사용하면 안된다. 227 | 228 | 2-2와 같은 이유인데, 메인 스레드에서 `sync`로 작업을 보내면 메인 스레드가 교착상태가 되어버린다 ! 이건 걍 무조건 교착상태가 되기 때문에 아예 컴파일 에러가 떠버려유 ~ 229 | 230 | 3. 객체에 대한 캡처를 주의해야한다. 231 | 232 | 동작해야 할 작업을 queue에 보낸다는 것은 결국 클로저를 보내는 것을 말한다. 233 | 234 | 따라서 객체에 대한 캡처 현상이 발생할 수 있으며, 이는 자칫하면 순환참조 문제를 일으킬 수 있다. 235 | 236 | 웬만하면 `[weak self]` 붙여주자 ~ ^^ 방어적 프로그래밍 굿뜨 ~ 237 | 238 | ### main, global 239 | 240 | DispatchQueue는 main과 global()을 통해 메인스레드에 작업을 할당하거나 바로 비동기 맥락으로 작업을 실행할 수 있다. 241 | 242 | main은 프로세스에서 하나만 존재하며, iOS에서는 UI를 담당한다. 243 | 244 | - UI이외의 작업이 메인 스레드에서 동작하는 것은 상관없다. (다만 UI응답시간이 길어짐) 245 | - UI 작업이 메인 외의 스레드에서 동작하면 런타임에 오류를 던진다. 246 | 247 | `global()`를 이용하면 시스템에서 제공하는 DispatchQueue를 이용하게 된다. 위에서는 custom queue를 만들어서 사용했다. 일반적으로 custom queue를 남발하면 스레드가 많아져서 context switch에서 오버헤드가 발생할 수 있으므로 `global()`사용을 권장한다. 248 | 249 | 다만, `global()`은 concurrent로 동작하기에 작업의 순서가 중요하다면 별도로 custom queue를 만들면 된다. 250 | 251 | - 이때 target을 `global()`로 설정하면 시스템에서 제공하는 스레드 풀을 공유한다. 252 | - Target? 253 | - 서로 다른 Queue가 각자에 대한 순서를 유지하면서도 같은 맥락에서 함께 수행하는 것이 가능하다. 254 | ![image-9](https://github.com/user-attachments/assets/3ca7365f-07c9-44b2-afc4-e6ef726c8aac) 255 | ![image-10](https://github.com/user-attachments/assets/fb37c9c6-65ff-4134-8dcf-1e1ceafcee9e) 256 | Q1과 Q2는 순서가 유지되지만, 하나의 EQ라는 Queue에서 실행된다. 257 | 이렇게 하면 Q1과 Q2간에 context switching을 줄이고 EQ하나로 통합할 수 있다. 258 | 259 | # DispatchGroup 260 | 261 | 하나의 작업이 무거울 수 있다. 262 | 263 | 만약 아주 오래걸리는 작업을 하나의 task로 dispatch queue에 넣으면 해당 작업을 수행하는데 오래 걸릴 수 있다. 264 | 265 | 이럴 때 DispatchGroup을 이용하면 된다. 266 | 267 | DispatchGroup은 여러개의 작업을 하나로 관리할 수 있다. 268 | 269 | 또한 작업의 완료 시점을 알고 원하는 작업을 수행할 수 있다. 270 | 271 | ```swift 272 | // DispatchGroup 생성 273 | let dispatchGroup = DispatchGroup() 274 | 275 | // dispatchGroup에 작업 추가 276 | DispatchQueue.global().async(group: dispatchGroup) { 277 | Logger().log("gergerg") 278 | sleep(3) 279 | } 280 | 281 | // 작업 종료 안내 282 | dispatchGroup.notify(queue: DispatchQueue.main) { 283 | Logger().log("done") 284 | } 285 | ``` 286 | 287 | 이렇게 하면 작업이 끝났을 때 알림을 받고 안의 함수블럭을 실행한다. 288 | 289 | # DispatchWorkItem 290 | 291 | 지금까지 큐에 작업을 넘길 때 클로저 안에 넣어서 처리했다. 292 | 293 | ```swift 294 | DispatchQueue.global().async { 295 | print("Task 시작") 296 | print("Task 끝") 297 | } 298 | ``` 299 | 300 | 클로저를 묶어 클래스로 캡슐화한 것이 **DispatchWorkItem** 이다. 301 | 302 | `DispatchWorkItem` 은 지금껏 클로저로 보내왔던 **작업이 캡슐화 된 class** 이다. 303 | 304 | ![image-11](https://github.com/user-attachments/assets/37a991c8-1e50-4597-b4c1-ffa76f22168f) 305 | 306 | 아래 예시 코드를 보면 알 수 있듯 `DispatchWorkItem`을 생성할 때 `qos` 파라미터를 통해 작업의 우선순위도 설정할 수 있다. 307 | 308 | ```swift 309 | let defaultItem = DispatchWorkItem { // Task } 310 | let utilityItem = DispatchWorkItem(qos: .utility) { // Task } 311 | ``` 312 | 313 | 그리고 이렇게 정의 된 `DispatchWorkItem`은 `async(execute:)` 라는 `DispatchQueue`의 인스턴스 메소드를 통해 큐로 보낼 수 있다. 314 | 315 | ```swift 316 | let utilityItem = DispatchWorkItem(qos: .utility) { // Task } 317 | 318 | DispatchQueue.global().async(execute: utilityItem) 319 | ``` 320 | 321 | 혹은 perform() 메소드를 통해 현재 스레드에서 sync하게 동작시킬 수 도 있다. 322 | 323 | ```swift 324 | utilityItem.perform() 325 | ``` 326 | 327 | ## DispatchWorkItem 기능 328 | 329 | `DispatchWorkItem`은 아래 두 가지 기능을 제공한다. 330 | 331 | 1. 취소 기능 332 | 333 | `DispatchWorkItem` 은 `cancel()` 이라는 인스턴스 메소드를 가지고 있다. 334 | 335 | ```swift 336 | let item = DispatchWorkItem { } 337 | item.cancel() 338 | ``` 339 | 340 | `cancel()` 메소드는 작업의 실행 여부에 따라 동작이 조금 달라진다. 341 | 342 | 작업이 아직 큐에 있고 실행되기 전에 `cancel()` 을 호출하면 작업이 제거된다. 343 | 344 | 만약 실행 중인 작업에 `cancel()` 을 호출하는 경우, 작업이 멈추지는 않고 `DispatchWorkItem`의 속성인 `isCancelled`가 `true`로 설정된다. 345 | 346 | 2. 순서 기능 347 | 348 | `notify(queue:execute:)` 라는 함수를 통해 작업 A가 끝난 후 작업 B가 특정 queue에서 실행되도록 지정할 수 있다. 349 | 350 | ```swift 351 | let itemA = DispatchWorkItem { } 352 | let itemB = DispatchWorkItem { } 353 | 354 | itemA.notify(queue: DispatchQueue.global(), execute: itemB) 355 | ``` 356 | 357 | # DispatchSemaphore 358 | 359 | `DispatchSemaphore`는 iOS에서 세마포어를 사용하기 위해 쓰이는 객체이다 .ᐟ.ᐟ 360 | 361 | ```swift 362 | // 공유 자원에 접근 가능한 작업 수를 2개로 제한 363 | let semaphore = DispatchSemaphore(value: 2) 364 | ``` 365 | 366 | 위와 같이 공유 자원에 접근 가능한 작업 수를 명시하고, 임계 구역에 들어갈 때에는 semaphore의 `wait()`을, 나올 땐 `signal()` 메소드를 호출한다. 367 | 368 | ```swift 369 | for i in 1...3 { 370 | semaphore.wait() // semaphore 감소 371 | DispatchQueue.global().async() { 372 | // 임계 구역 373 | print("공유 자원 접근 start") 374 | sleep(3) 375 | 376 | print("공유 자원 접근 end") 377 | semaphore.signal() // semaphore 증가 378 | } 379 | } 380 | 381 | ``` 382 | 383 | `DispatchSemaphore`는 두 가지 방식으로 사용할 수 있는데, 하나는 위와 같은 방식처럼 동시 작업 개수를 제한하는 것이다. 384 | 385 | 또 다른 하나는 **두 스레드가 특정 이벤트의 완료 상태를 동기화 하는 경우**에 유용하다. 386 | 387 | 이 용도로 사용할 때에는 `DispatchSemaphore`의 초기값을 0으로 설정하면 된다. 388 | 389 | ```swift 390 | let semaphore = DispatchSemaphore(value: 0) 391 | 392 | print("task A가 끝나길 기다리는 중..") 393 | 394 | DispatchQueue.global().async() { 395 | // task A 396 | print("task A start..") 397 | print("task A end..") 398 | 399 | // task A 끝났다고 알려쥼 ~ 400 | semaphore.signal() 401 | } 402 | 403 | // task A 끝날 때 까지는 value가 0이므로, task A 종료까지 block 404 | semaphore.wait() 405 | print("task A 완료 ~") 406 | 407 | ``` 408 | 409 | # DispatchBarrier 410 | 411 | **DispatchBarrier**는 *“concurrent dispatch queue에서 실행되고 있는 task들에 대한 동기화 지점”* 라고 공식문서에서 말한다. 412 | 413 | concurrent queue에 barrier를 추가하면, queue는 이전에 제출된 모든 작업이 실행을 마칠 때까지 barrier block의 실행을 지연시켰다가 앞에 제출된 작업들이 모두 끝나면 barrier block을 자체적으로 실행한다고 한다. 414 | 415 | 그럼 사용 방법은 다음과 같다. 416 | 417 | dispatch queue의 인스턴스 메소드인 `async(group:qos:flags:execute:)` 를 사용해서 `flags` 파타미터에 `.barrier`를 넣어주면 된다. 418 | 419 | ```swift 420 | concurrentQueue.async(flags: .barrier, execute: { }) 421 | ``` 422 | 423 | > ### 효준이가 헷갈렸던 것 424 | > 425 | > %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202025-01-17%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%201 07 35 426 | > 427 | > - 만약 프로세서가 1개라면 위 작업이 끝나면 `6+a`초 일까, `3+a` 초 일까? 428 | 6 + a 초임 429 | 결국 CPU는 한 번에 하나의 작업밖에 처리하지 못 하기 때문에 멀티 스레드로 돌려봤자 컨텍스트 스위칭하느라 6초에 a시간만큼 더 소요되는 것 430 | 멀티코어 프로세서에서 멀티 스레드를 해야 효과가 있음 431 | > - 그럼 단일 코어 프로세서에서 멀티 스레드를 하면 이점이 없나 ? 432 | 답은 No 433 | 단일 코어여도 멀티스레드는 장점이 있음 434 | 작업을 비동기적으로 처리할 수 있으므로 I/O 작업에서 이점을 얻을 수 있음 435 | I/O 요청하고 자기 할 일 하러 가면 되니까 436 | 437 | # QNA 438 | 439 | - GCD에서 동기/비동기, 직렬/동시 큐에 대해 설명해주세요. 440 | - GCD의 주요 구성 요소는 어떤 것들이 있나요. 441 | - GCD에서 우선순위 역전을 방지하기 위해 어떻게 할까요. 442 | - 동시성 프로그래밍은 무엇일까요. 443 | - 병렬 프로그래밍과 동시성 프로그래밍의 차이점은? 444 | - 동시성 프로그래밍을 하려면 코어나 프로세서를 사용하지 않고 어떻게 구현하나요? 445 | - iOS에서 동시성 프로그래밍 방법은? 446 | - GCD의 동작 원리에 대해 설명해주세요. 447 | - GCD와 Operation Queue의 차이점은? 448 | -------------------------------------------------------------------------------- /Week4/swift concurrency.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | 3 | - [Swift Concurrency 등장 배경](#swift-concurrency-등장-배경) 4 | - [가독성 & 에러처리 관점](#가독성--에러처리-관점) 5 | - [가독성](#가독성) 6 | - [에러 핸들링 안정성](#에러-핸들링-안정성) 7 | - [성능적 관점](#성능적-관점) 8 | - [스레드 관점](#스레드-관점) 9 | - [우선순위 역전](#우선순위-역전) 10 | - [CompletionHandler → Async/await](#completionhandler--asyncawait) 11 | - [스레드 제어권](#스레드-제어권) 12 | - [스레드 제어권 관점](#스레드-제어권-관점) 13 | - [sync 에서의 스레드 제어권](#sync-에서의-스레드-제어권) 14 | - [async 에서의 스레드 제어권](#async-에서의-스레드-제어권) 15 | - [스택 프레임의 변화](#스택-프레임의-변화) 16 | - [sync 방식의 Stack Frame](#async-방식의-stack-frame) 17 | - [async 방식의 Stack Frame](#async-방식의-stack-frame) 18 | - [add가 호출된 상황](#add가-호출된-상황) 19 | - [await database.save가 호출된 경우](#await-databasesave가-호출된-경우) 20 | - [save 메소드 종료 후 Return 과정](#save-메소드-종료-후-return-과정) 21 | - [async let](#async-let) 22 | - [async-let 예제](#async-let-예제) 23 | - [Task](#task) 24 | - [Structured Concurrency](#structured-concurrency) 25 | - [Task Group](#task-group) 26 | - [AsyncSequence/Stream](#asyncsequencestream) 27 | - [협력적 취소](#협력적-취소) 28 | - [Continuation](#continuation) 29 | - [Swift Concurrency](#swift-concurrency) 30 | - [참고자료](#참고자료) 31 | 32 | # Swift Concurrency 등장 배경 33 | 34 | > swift concurrency란? 35 | WWDC 2021년에 소개된 동시성 프로그래밍 API 36 | > 37 | 38 | async와 await 키워드로 비동기 태스크 종료 후 코드를 작성할 수 있다. 39 | 40 | await로 중지되면, 이후 사용해야 하는 데이터를 Heap 영역에 저장하고, 이후 다시 돌아오면 꺼내서 사용한다. 41 | 42 | GCD와 비교하며 왜 등장하게 되었는지 살펴보겠다. 43 | 44 | ## 가독성 & 에러처리 관점 45 | 46 | ### 가독성 47 | 48 | image 49 | 50 | 51 | 기존의 GCD는 비동기 작업이 끝났는 지의 여부를 Completion Closure를 통해 알려준다. 52 | 53 | 그러면 A 작업이 끝나면 B, B 작업이 끝나면 C, … 이를 비동기로 처리한다면 무수히 많은 Depth가 생겨 들여쓰기에 의해 가독성이 낮아질 것이다. 54 | 55 | 반면 Swift Concurrency는 아래와 같이 동작한다. 56 | 57 | image 1 58 | 59 | 60 | 위 사진들의 코드는 동일한 로직인 것. 61 | 62 | await 키워드를 통해 실제 비동기 코드이지만, **동기처럼** 보이게 하는 효과를 지녀 가독성을 증가시킬 수 있다. 63 | 64 | ### 에러 핸들링 안정성 65 | 66 | URLSession을 통해서 이미지를 다운 받는 메소드가 있다고 하자 67 | 68 | 이미지 내려받는 걸 실패했을 때 예외처리하는 상황으로 둘을 비교해보겠다. 69 | 70 | image 2 71 | 72 | 73 | GCD는 이미지를 성공적으로 내려받으면 컴플리션 핸들러의 첫 번째 파라미터로 이미지를 넘겨준다. 74 | 75 | 그러나 상태코드가 200이 아니거나, 내려받은 data가 Nil인 경우 nil을 줘야 한다. 76 | 77 | 개발자가 실패했을 때에 대한 에러처리를 잘 하면 문제가 없지만, 휴먼 에러등의 이유로 컴플리션 클로저 호출을 빼먹으면 문제가 될 수 있다. 78 | 79 | 매번 확인해야 하는 번거로움이 있음 80 | 81 | 추가로, Result를 쓰면 가독성은 더 심각해짐 82 | 83 | image 3 84 | 85 | 86 | 그래서 Swift Concurrency에서는 컴플리션 핸들러를 사용하지 않는다. 87 | 88 | 대신 do-catch 혹은 gaurd에 의해 Error를 던져주는 식으로 처리를 할 수 있는 것. 89 | 90 | 이러면 실패했을 때 컴플리션 핸들러를 빼먹어도 문제가 되지 않는다. 91 | 92 | + 콜백을 안 해도 되니까 가독성도 좋아짐 93 | 94 | ## 성능적 관점 95 | 96 | > 스레드 생성량과 Context Switching 수를 비교해 보는 과정 97 | > 98 | 99 | ### 스레드 관점 100 | **GCD** 101 | 102 | GCD는 Thread Explosion을 조심해야 한다. (폭발이 아니라 너무 많이 생성되는 것 ㅇㅇ) 103 | 104 | 스레드를 너무 많이 만들면 컨텍스트 스위칭이 많아지고, 성능이 오히려 저하된다. 105 | 106 | 너무 많은 스레드 블록에서의 메모리 오버헤드, 스케줄링 오버헤드 등이 문제라 Thread Explosion을 예방하는 안전한 코드 작성이 필요하게 된다. 107 | 108 |
109 | 110 | **Swift Concurrency** 111 | 112 | 반면, Swift Concurrency에서의 동작 113 | 114 | 비동기 컨텍스트에서 await로 비동기(= async로 선언된) 메소드를 호출할 경우, 동일한 스코프의 await 아래에 있는 코드들은 비동기 메소드가 끝날 때까지 대기한다. 115 | 116 | CPU 제어권을 갖고 있는 스레드가 위 지점을 만나면, 시스템에게 `async 메소드의 작업을 해야해!`라고 알려준다. 또한, 해당 스레드는 CPU 제어권을 포기하여 다른 작업을 해당 스레드에서 할 수 있게 준비한다. 117 | 118 |
119 | 120 | 위처럼 하는 이유는 다음과 같다. 121 | 122 | 스레드가 무의미하게 CPU 제어권을 잡아 낭비하는 현상을 막을 수 있고, 시스템은 제어권이 없는 스레드에게 우선순위가 높은 작업들을 할당해줄 수 있게 된다. 123 | 124 | GCD와 달리 스레드를 무작정 만들어서 비동기 작업을 진행하는 게 아니라, 위처럼 스레드를 재사용하며 처리한다. 125 | 126 | 그리고 기존의 GCD에서 컨텍스트 스위칭으로 스레드 별로 작업을 처리했다면, Swift Concurrency에서는 위 방식으로 `실행할 작업을 함수 호출 수준의 비용`만으로 해결했다. 127 | 128 | 등장 배경에 대한 개요라 두괄식으로 장점만 적었는데, 이에 대한 원리와 더 자세한 설명은 아래 `async 에서의 스레드 제어권`과 `스택 프레임`에서 설명하겠다. 129 | 130 | image 4 131 | 132 | 후술하겠지만 Swift Concurrency에서는 Actor가 Thread를 재활용하고, 133 | 134 | Thread의 개수를 Core의 개수와 동일하게 제한해서 이 문제를 해결한다. 135 | 136 | ### 우선순위 역전 137 | 138 | GCD로 동시성 프로그래밍을 할 경우, 우선순위 역전이 발생할 수 있다고 한다. 139 | 140 | 하나의 큐에서 QoS가 각기 다른 작업이 담길 수 있는 것. 141 | 142 | Background QoS인 작업이 큐에 추가되고, User Initiated 작업이 추가됐다고 가정하겠다. 143 | 144 | 그러면 **background 작업들의 우선순위를 User Initiated로 올려서** 새로 추가된 태스크가 너무 기다리지 않게 함 145 | 146 | 이게 FIFO 방식이라 그런듯 147 | 148 | 반면 Swift Concurrency는 FIFO가 아니므로 우선순위가 높은 애들을 먼저 처리해줄 수 있음 149 | 150 | Task에 priority를 부여해서 앞에 작업이 쌓여있더라도 높은 우선순위 작업이 들어오면 해당 작업 먼저 수행시킬 수 있다. 151 | 152 | image 5 153 | 154 | 155 | ## CompletionHandler → Async/await 156 | 157 | 서버에서 이미지 리스트를 불러오고 이미지에 대한 썸네일을 화면에 보여주는 과정이 있다고 해보자. 서버에서 가져온 정보를 `UIImage`로 변환하는 과정에는 아래와 같은 일련의 과정이 필요하다. 158 | 159 | image 6 160 | 161 | 162 | 해당 과정을 살펴보면 하위 과정이 실행되기 위해서는 상위 과정에 대한 결과값이 필요하다. 즉, 위 과정들은 모두 차례대로 진행되어야함을 의미한다. 163 | 164 | `thumbnailURLRequest`나 데이터를 UIImage로 전환하는 `UIImage(data:)` 와 같은 메서드들은 결과가 매우 빠르게 도출되기 때문에 어떤 스레드에서 호출되어도 상관없으며 동기적으로 실행되어도 괜찮다. 165 | 166 | 하지만 `dataTask(with:completion:)` 이나 `prepareThumbnail(of:completionHandler:)` 와 같은 함수들은 실행하고 결과가 나오기까지 시간이 조금 걸린다. 따라서 SDK에선 비동기 함수를 제공하며 위와 같은 함수들은 비동기로 실행되어야한다. 167 | 168 | 그럼 위 과정을 이제 기존의 `completionHandler`를 통한 비동기 처리 방식으로 코드를 짜보자. 169 | 170 | image 7 171 | 172 | 173 | 먼저 `thumbnailURLRequest(for:)` 메소드를 호출한다. 위에서 말했듯 이 함수는 동기적으로 호출되는 함수이기 때문에 빠르게 처리가 된다. 174 | 175 | image 8 176 | 177 | 이후 `URLSessionDataTask` 를 동기적으로 만들고 비동기 작업을 시작하기 위해 따로 `task.resume()` 를 호출해야한다. 178 | 179 | 데이터를 다운로드 받는 것은 시간이 걸리는 작업이며, 그 동안 스레드가 block되지 않게 하기 위해서는 위와 같이 비동기 작업으로 처리해주는 것이 매!우! 중요하다. 180 | 181 | image 9 182 | 183 | 184 | 다운로드 요청이 완료되면 completionHandler를 통해 data, response, error 값들이 옵셔널하게 도착한다. 185 | 186 | 만약 error가 발생했다면, completionHandler를 호출하여 에러 처리를 해줘야한다. 187 | 188 | image 10 189 | 190 | 191 | 값이 잘 도착했다면, `UIImage(data:)` 를 호출하여 동기적으로 데이터를 `UIImage`로 변환시켜준다. 192 | 193 | image 11 194 | 195 | 이미지가 잘 생성이 되었다면, 우리는 `prepareThumbnail` 메소드를 호출하고 또 completionHandler를 통해 값을 전달한다. 해당 과정이 이루어지는 동안 스레드는 unblocked되고 다른 작업을 할 수 있게 된다. 196 | 197 | 간단한 과정인데 일단 굉장히 장황하게 설명되었다.. 그럼 위 코드는 이제 완-벽 한걸까?? 198 | 199 | 노노 .ᐟ.ᐟ 200 | 201 | image 12 202 | 203 | 204 | 위 `guard-let` 구문을 보면 에러에 대한 처리 없이 그냥 함수를 종료시켜버린다 ! 따라서 UIImage를 생성하거나 썸네일을 생성하는데 실패했더라도, `fetchThumbnail` 의 호출부는 이를 알 수 없고, 이미지는 영영 업데이트 되지 않게된다.. 205 | 206 | image 13 207 | 208 | 209 | 이를 해결하기 위해선 모든 함수 return 경로에 error를 담은 completion을 호출해야한다.. 여기선 Swift의 기본 에러 핸들링 메커니즘을 사용할 수 없는 것이다. (error throw하는거 못 함;) 210 | 211 | 이렇게 completionHandler를 사용한 두 개의 동기, 두 개의 비동기 처리를 하는 함수를 완성시켰다. 212 | 213 | 근데 20줄 따리의 코드 중 무려 5줄의 미묘한 버그가 끼어있을 수 있는 에러를 담은 completionHandler가 끼어있다. ㅋㅋ 이게 맞냐고 ~ 214 | 215 | 이걸 아 ~ 주 조금은 안전하게 만들 수 있다. 바로 Result 타입을 활용하는 방식이다. 216 | 217 | image 14 218 | 219 | 음.. 근데 그냥 코드가 조금 더 길어지고 못생겨짐.. 220 | 221 | 자 ~ 그럼 이제 이 못나고 불안전한 코드를 async/await을 활용하여 리팩토링해보자 ! 222 | 223 | image 15 224 | 225 | 먼저 함수를 작성할 때, `throws` 키워드 전에 `async` 키워드를 붙여준다. 에러를 던지지 않는 함수라면 그냥 화살표 전에 `async`를 붙여주면 된다. 226 | 227 | 그럼 이제 깔꼼하게 `fetchThumbnail` 함수는 UIImage를 반환하고, 에러가 발생하면 throw를 할 수 있게 되었다 ! 228 | 229 | image 16 230 | 231 | 맨 처음 `fetchThumbnail` 이 호출되면, 이 전과 같이 `thumbnailURLRequest` 를 호출한다. 이 함수는 동기함수로, 해당 작업을 하는 동안은 스레드가 block 된다. 232 | 233 | image 17 234 | 235 | 다음으론 `data(for:)` 메소드를 호출하여 데이터를 다운받기 시작한다. `data(for:)` 메소드는 `dataTask` 와 같이 Foundation에서 제공하는 메소드로, 비동기적으로 처리된다. 하지만 `dataTask`와는 달리, `data(for:)` 메소드는 **awaitable**하다. 해당 함수가 호출되면, 빠르게 중단되고, 스레드는 unblocked 되며 다른 작업을 할 수 있게 된다. 236 | 237 | `throws` 키워드가 붙은 함수를 호출하기 위해서 `try`를 붙여야 하는 것처럼, `async` 키워드가 붙은 함수를 호출하기 위해선 `await` 키워드를 붙여줘야한다. 238 | 239 | `dataTask`와 `data` 두 버전 모두 값과 에러에 대한 처리를 제공하고 있다. 하지만 awaitable 버전은 훨씬훨씬 코드가 간단해진 것을 확인할 수 있다 ! 240 | 241 | image 18 242 | 243 | 이후 데이터를 `UIImage`로 변환시키고 thumbnail 프로퍼티에 접근하면 썸네일이 렌더링되기 시작한다. 썸네일이 생성되기 시작하면, 스레드는 또 다시 unblocked 되며 다른 작업을 할 수 있게 된다. 그리고 썸네일이 잘 생성되었다면 그걸 반환하고, 실패했다면 error를 throw하게 된다. 244 | 245 | 껄껄.. completionHandler로는 20줄이었던 코드가 단 6줄로 변-신 ~ 246 | 247 | 심지어 depth가 깊어지지도 않은 완전 straight한 코드임.. 248 | 249 | 위 코드에서 확인할 수 있듯, `async` 키워드는 함수에만 붙을 수 있는게 아니고 프로퍼티, 이니셜라이저 등에도 모두 붙일 수 있다. 250 | 251 | 저 `thumbnail`이라는 프로퍼티는 기본제공이 아니고 따로 만든 프로퍼티인데 그 코드를 살펴보자. 252 | 253 | image 19 254 | 255 | UIImage의 extension에 구현해줬는데, 구현부는 굉장히 짧다. thumbnail 프로퍼티는 CGSize를 만들고 `byPreparingThumbnail(ofSize:)` 의 결과를 기다린다. 256 | 257 | 프로퍼티가 async 키워드를 가지기 위해 필요한 사항이 몇 가지 있다. 258 | 259 | 첫 번째로, 명시적 `getter` (explicit getter)가 있어야 한다는 점이다. async 키워드를 붙이기 위해선 `getter`를 명시적으로 적어줘야한다. 추가로 Swift 5.5부터는 `getter`에도 `throws` 키워드가 붙일 수 있다. 260 | 261 | 두 번째로는, 프로퍼티가 `setter`를 가져서는 안된다. 프로퍼티에 `async` 키워드가 붙기 위해서는 read-only 여야만 한다. 262 | 263 | 함수나 프로퍼티, 이니셜라이저에서 `await` 키워드는 함수가 어디서 스레드를 unblock할 것인지를 나타낸다. `await` 키워드는 다른 곳에서도 사용될 수 있다. 264 | 265 | image 20 266 | 267 | `async` 시퀀스를 반복하기위한 반복문에서 위와 같이 사용할 수 있다. 비동기 시퀀스는 각각의 요소들을 비동기적으로 제공한다는 점을 제외하고는 일반 시퀀스와 같다. 따라서 다음 요소를 가져오기 위해선 `await` 키워드가 붙어야하며, 이는 해당 요소가 `async`임을 나타낸다. 268 | 269 | (여기서 이제 AsyncSequence에 대해 더 알고싶으면 [Meet AsyncSequence](https://developer.apple.com/videos/play/wwdc2021/10058/)로, 많은 비동기 작업들을 병렬적으로 실행하는 것을 알고싶다면 [Structed concurrency in Swift](https://developer.apple.com/videos/play/wwdc2021/10134/)로..) 270 | 271 | # 스레드 제어권 272 | 273 | await로 비동기 메소드를 호출하는 경우, Potential Suspension Point로 지정된다. 274 | 275 | image 21 276 | 277 | 생각해보면 당연하다 278 | 279 | await로 URLSession의 data 비동기 메소드를 호출하면 그 아래 작업들은 data 메소드가 끝날 때 까지 기다리게 된다. 280 | 281 | 이 지점을 `Suspension Point` 라고 한다. 282 | 283 | 이를 통해 fetchThumbnail의 메소드는 더 이상 할 일이 없으니, 해당 작업을 처리하던 스레드가 다른 동작을 할 수 있게끔 제어권을 놓아주는 행위를 할 수 있다. 284 | 285 | 스레드를 멈추는 것이 아닌, 다른 작업을 할 수 있게 제어권을 넘기는 것 말이다. 286 | 287 | Suspend 된다 = 해당 스레드에 대한 제어권을 포기한다 288 | 289 | 라고 봐도 무방할듯 290 | 291 | ## 스레드 제어권 관점 292 | 293 | ### sync 에서의 스레드 제어권 294 | 295 | A 함수에서 B라는 sync 동기 함수를 호출하면, A 함수가 실행되던 스레드의 제어권을 B 함수에게 전달한다. 296 | 297 | A 함수는 동기적으로 호출했기 때문에 B가 끝날 때까지 아무것도 하지 못 한다. 298 | 299 | image 22 300 | 301 | 따라서 하나의 스레드에서 A가 동작하다가 B 작업을 하고, B가 끝나면 A 작업으로 다시 돌아온다. 302 | 303 | 이게 sync의 스레드 점유권의 흐름이다. 304 | 305 | ### async 에서의 스레드 제어권 306 | 307 | 개요에서 말했듯 A 작업을 하다 B라는 비동기 메소드를 호출하면 A는 스레드 제어권을 B에게 넘겨준다. 308 | 309 | 왜냐하면 A 작업은 어차피 B를 호출한 시점부터 그 아래 코드들은 B가 끝날 때까지 아무것도 못 하기 때문이다. 310 | 311 | image 23 312 | 313 | 그러면 Suspension Point를 만난 순간부터 스레드 제어권을 포기하면, 314 | 315 | 해당 **스레드에 대한 제어권은 시스템에게 가고** 시스템은 스레드를 사용해서 다른 작업을 수행할 수 있게 된다. 316 | 317 | 우선순위에 따라 여러 작업을 멀티 스레드로 처리할 것이다. 318 | 319 | 그러다 **멈췄던 내 작업이** 가장 중요하다고 판단되는 순간에 **해당 함수를 재개(resume)**하고, 비동기 함수는 할당받은 스레드를 **다시 제어하며 작업**할 수 있게 된다. 320 | 321 | 1. await로 async 함수를 호출하는 순간(= Suspension Point) 해당 스레드 제어권 포기 322 | 2. async 작업 및 같은 블록 아래의 코드들은 실행 불가능 323 | 3. 스레드 제어권을 시스템에게 넘기면서, 1번의 호출된 async도 스케줄 대상이 됨 324 | 4. 시스템은 작업 우선순위를 따지며 작업들을 처리하고, 325 | 이때 1번이 실행되던 스레드에서 다른 작업을 먼저 실행할 수도 있음 326 | 5. 그러다 1번의 호출된 async 작업이 중요해지는 순간(= 내 차례) 327 | 다시 작업하라고 resume을 하고, 이 때 특정 스레드의 제어권을 줘서 마저 실행이 된다. 328 | 329 | `중요한 건 이때 Resume되는 스레드는 1번 스레드와 다를 수 있음` 330 | 331 | 332 | await한다고 **무조건** Suspension Point가 되는 건 아니지만, 333 | 334 | 위처럼 await 키워드를 통해 코드 블럭이 하나의 트랜잭션으로 처리되지 않을 수 있음 335 | 336 | # 스택 프레임의 변화 337 | 338 | ## sync 방식의 Stack Frame 339 | 340 | 모든 스레드는 함수 호출을 위한 자신만의 독립된 스택 영역을 갖는다. 341 | 342 | image 24 343 | 344 | 스레드가 함수 호출을 실행하면 새 프레임이 스택에 푸쉬, 345 | 346 | 해당 스택 프레임은 스택의 Top에 쌓이고 이에는 로컬 변수, 리턴 주소값 등이 포함되어 있다. 347 | 348 | 쌓인 스택 프레임은 함수가 끝나면 Pop 되어 사라진다. 349 | 350 | ## async 방식의 Stack Frame 351 | 352 | image 25 353 | 354 | 1. 비동기 메소드인 `updateDatabase`를 호출 355 | 2. `updateDatabase` 내에서 비동기 메소드인 `add` 호출 356 | 3. add 내에서 비동기 메소드인 `database.save` 호출 357 | 358 | Flow는 위와 같다. 359 | 360 | ### add가 호출된 상황 361 | 362 | 먼저 2번, add가 호출된 상황부터 보면 363 | 364 | 스택 메모리 관점에서는 add 메소드가 호출됐으니 add 메소드에 대한 스택 프레임을 스택 영역에 적재한다. 365 | 366 | `중요` 367 | 368 | 이때, add 스택 프레임에는 사용할 필요가 없는 Local 변수를 저장한다. 369 | 370 | 무슨 뜻이냐면, suspension point 때문에 사용되지 않을 (= await 아래) 지역 변수를 스택 프레임 저장한다는 것이다. 371 | 372 | 그럼 위 사진과 같이 `(id, article)`이 스택 프레임에 담기게 될 것이다. 373 | 374 | 왜 이렇게 하냐면, **await 전/후로 모두 사용되는 정보를 저장하기 위한 공간이 필요**하다. 375 | 376 | 그럼 await 전에 존재하는 지역변수(= 파라미터) `newArticle`은 따로 저장 공간이 필요할 것이다. 377 | 378 | **Suspension Point를 만나서 다른 스레드로 작업을 이어갈 때,** 이 전 내용들을 기억하기 위해 **Heap 메모리 영역에 저장**한다. 379 | 380 | image 26 381 | 382 | 위와 같이 말이다. 383 | 384 | ### await database.save가 호출된 경우 385 | 386 | 이어서 3번, add 함수에서 await database.save를 호출한 경우 이 곳이 Suspension Point가 된다. 387 | 388 | 그러면 스택 영역 제일 위에 있던 add 스택 프레임의 변화를 보자 389 | 390 | 이론 상, A 메소드가 호출되다가 B 메소드를 호출하면 A 스택 프레임 위에 B가 쌓이게 된다. 그러면서 B 메소드가 동작이 끝나면 다시 A로 돌아와서 기존 작업을 한다. 391 | 392 | 그러나, 비동기 메소드에서 비동기 메소드의 경우 add 스택 프레임 위에 쌓이는 것이 아니라, **add 스택 프레임이 save 스택 프레임으로 대체**된다. 393 | 394 | **중요** 395 | 396 | 이렇게 동작하는 이유는 **await 전후로 사용될 코드가 Heap 영역의 async frame에 저장**되어 있기 때문에 스택에 필요하지 않는 것이다. 397 | 398 | 그리고 **스택에 있어봤자 스레드 점유권을 다시 얻을 때 해당 스레드로 돌아온다는 보장이 없기 때문**이다. 399 | 400 | Suspension Point에서 모든 정보가 Heap에 저장되니, 다시 점유권을 얻었을 때 작업 수행이 가능한 것이다. 401 | 402 | 이 async 프레임 목록은 Continuation에 대한 런타임의 표현이다. 403 | 404 | image 27 405 | 406 | 따라서 3번이 끝나면, 위 사진처럼 save 스택 프레임이 add 스택 프레임을 대체하게 된다. 407 | 408 | ### save 메소드 동작 중 await 409 | 410 | image 28 411 | 412 | save 메소드 내부에서 만약 await로 비동기 메소드를 호출해서 Suspend가 되었다고 가정하겠다. 413 | 414 | 그러면 해당 스레드는 스레드 점유권을 내주고 되고 해당 스레드는 다른 작업을 수행할 수 있게 된다. 415 | 416 | ### save 메소드 종료 후 Return 과정 417 | 418 | save 메소드가 동작할 차례가 되어 continuation에 의해 Heap에 있던 save 비동기 프레임이 스택에 쌓이게 되고, 419 | 420 | save 메소드 수행 후 작업을 마치면 [ID]를 반환한다. 421 | 422 | image 29 423 | 424 | save 메소드의 동작이 끝나면 [ID]를 반환하고, save를 위한 스택 프레임은 add 메소드를 위한 스택 프레임으로 대체된다. 425 | 426 | # async let 427 | 428 | > `동시 바인딩`을 지원하고자 나온 Task의 한 종류 429 | > 430 | 431 | ```swift 432 | func fetchOneThumbnail(withID id: String) async throws -> UIImage { 433 | let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id) 434 | async let (data, _) = URLSession.shared.data(for: imageReq) 435 | async let (metadata, _) = URLSession.shared.data(for: metadataReq) 436 | guard let size = parseSize(from: try await metadata), 437 | let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) 438 | else { 439 | throw ThumbnailFailedError() 440 | } 441 | return image 442 | ``` 443 | 444 | 위의 코드처럼 async let 을 쓰면 해당 변수를 사용할 때 await으로 기다린 후에 사용할 수 있다. 445 | 446 | 왜냐하면 언제 작업이 끝날지 모르기 때문임! 447 | 448 | %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-01-24_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_8 47 07 449 | 450 | 이 경우 Swift는 자식 작업을 생성한 후 빈 값을 변수에 넣고 계속 진행시킨다. 451 | 452 | 그 후 해당 변수가 필요해질 때 실제로 자식 작업을 기다리게 된다. 453 | 454 | - 사실 우리는 변수를 r-value로 사용할 때 해당 변수의 get 함수를 실행한다. 455 | - 그런데 await하고 r-value를 사용한다는 의미는 무엇일까? 456 | - 즉, get의 async 버전이 존재한다는 것이다. 457 | 458 | ```swift 459 | class A { 460 | var a: Int { 461 | get async { 462 | return 1 // 대충 오래걸리는 작업 463 | } 464 | } 465 | } 466 | ``` 467 | 468 | ### async-let 예제 469 | 470 | image 30 471 | 472 | 두 가지 다른 URL로부터 데이터를 다운로드하는 예제가 있다. 473 | 474 | 현재 코드는 순차적 바인딩이다. 475 | 476 | 하나는 이미지를 받는 거고, 하나는 이미지에 대한 메타 데이터용. 477 | 478 | 이러면 imageReq를 통해 이미지를 받아올 때까지 기다리고, 479 | 480 | 그 후에 metadata를 받아올 때까지 기다려서 이미지를 만들고 반환을 하게 된다. 481 | 482 | 그리고 오류 가능성이 있기 때문에 `try await` 을 사용해서 호출해야 한다. 483 | 484 | async-let 도입 485 | 486 | image 31 487 | 488 | 두 다운로드가 동시에 이루어질 수 있게 async-let을 사용하여 동시 바인딩을 한다. 489 | 490 | 이러면 Child Task에서 작업이 발생하기 때문에 try await을 사용하지 않아도 됨 491 | 492 | image 32 493 | 494 | 이제 아래 블록들에서 data와 metadata 변수를 사용하기 전에 try await을 한다. 495 | 496 | %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-01-24_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_8 57 52 497 | 498 | 한 비동기 메소드에서 다른 비동기를 호출할 때마다 동일한 Task를 사용해서 호출한다. 499 | 500 | fetch~ 메소드에서 두 개의 **async-let으로 두 개의 자식 Task를 만든 것**. 501 | 502 | 이러니까 트리 구조가 되는 거고, 상위에서는 await를 할 필요 없이 작업이 완료된 경우에 만 부모가 작업을 완료할 수 있다고 말함 503 | 504 | # Task 505 | 506 | 위에서 만든 async 함수들을 어떻게 사용할까? 507 | 508 | 그냥 맨땅에 `await function()`하면 컴파일 에러가 발생한다. 509 | 510 | 비동기 함수를 호출하려면 비동기 맥락에서 사용해야 한다. 511 | 512 | 왜냐하면 await자체가 관리 권한을 포기할 수 있다는 것인데 main스레드처럼 sync환경에 실행하면 해당 실행이 정지할 수 있기 때문이다. 513 | 514 | 이를 위해 Task라는 특별한 구조체가 필요하다. 515 | 516 | ```swift 517 | Task { 518 | // 비동기 코드 실행 519 | } 520 | 521 | ``` 522 | 523 | Task를 생성하면 클로저로 전달된 작업이 바로 실행된다. (비동기로 실행됨!) 524 | 525 | 이 때 Task는 우선순위를 부여하여 생성할 수 있고 취소도 가능하다. 526 | 527 | ```swift 528 | let task = Task(priority: .background) { 529 | <#code#> 530 | } 531 | task.cancel() 532 | 533 | ``` 534 | 535 | 이렇게 취소가 가능하다. 536 | 537 | - task 변수에 할당하지 않아도 비동기 프로그램은 정상적으로 실행된다. 다만, cancel등의 관리를 할 수 없다는 단점이 생긴다. ⇒ 취사선택 538 | 539 | Task의 다른 특징 중 하나는 주변 환경을 캡쳐한다는 것이다. 540 | 541 | 이는 actor등과 같은 격리된 환경과 자신을 호출한 자료형에 대한 참조도 포함한다. 542 | 543 | - 그러나 Task에서는 그 실행이 종료되면 바로 self에 대한 레퍼런스를 내려놓는다. 544 | - 따라서 weak self로 캡쳐해야 하는 경우가 거의 없다. 545 | 546 | 만약 주변환경을 캡쳐하기 싫다면 → `Task.detached`를 사용하자. 547 | 548 | # Structured Concurrency 549 | 550 | Task하나만으로 비동기 코드를 실행할 수 있다니! 참 좋은데 말입니다… 551 | 552 | 그런데 Task 안에서도 다른 Task를 만들 수 있지 않을까? 553 | 554 | 비동기 작업 하나에 대해 오래 걸리는 작업을 또 분리하고 싶은 요구가 있을 수 있다. 555 | 556 | ```swift 557 | let task = Task { 558 | Task { 559 | 560 | } 561 | } 562 | 563 | ``` 564 | 565 | 요런식으로 말이다. 566 | 567 | 그런데 이렇게 하면 문제가 있다. 568 | 569 | - 첫번째 task의 경우 task외부에서 관리할 수 있다. 그러나 중첩된 Task는 외부에서 관리하기 힘들다. 570 | - 그리고, 이렇게 생성된 Task는 자신을 생성한 Task와는 별도로 동작한다. 571 | 572 | # Task Group 573 | 574 | 새로운 Task가 기존 Task에 종속된 관계를 갖게 할 수 없을까? 575 | 576 | 그래서 Task Group이 필요하다. 577 | 578 | ```swift 579 | Task { 580 | let arr: [Int] = await withTaskGroup(of: Int.self) { group in 581 | var arr = [Int]() 582 | group.addTask { 583 | 1 584 | } 585 | group.addTask { 586 | 1 587 | } 588 | for await int in group { 589 | arr.append(int) 590 | } 591 | //----- 592 | // while let int = await group.next() { 593 | // 594 | // } 595 | //----- 596 | // var it = group.makeAsyncIterator() 597 | // while let int = await it.next() { 598 | // 599 | // } 600 | return arr 601 | } 602 | } 603 | 604 | ``` 605 | 606 | TaskGroup은 비동기 함수라서 비동기 맥락 안에서 실행되어야 한다. 607 | 608 | 또한 2가지 정보가 필요하다. (자식 작업의 return 타입, 그룹 작업의 return 타입) 609 | 610 | 그룹 작업의 return 타입의 경우 대개 타입추론으로 해결해주는데 필요한 경우 직접 적어야 한다. 611 | 612 | 이렇게 하면 비슷한 작업에 대해 자식 작업을 만들어서 동시에 여러 자료를 취합할 수 있게 된다. 613 | 614 | 이 방식을 `구조적 동시성`이라고 한다. 615 | 616 | - 계층적으로 부모 - 자식 관계를 형성하고, 부모는 자식작업이 끝날때까지 기다린다. 617 | - 작업의 우선순위 = max(부모, 자식) 618 | 619 | 구조적 동시성 종류는 다음과 같다. 620 | 621 | - Task Group 622 | - async-let 623 | 624 | TaskGroup에는 Throwing할 수 있는 ThrowingTaskGroup이 별도로 있다. 625 | 626 | | **특성** | **TaskGroup** | **ThrowingTaskGroup** | 627 | | --- | --- | --- | 628 | | **정의** | 일반 작업 그룹으로, 작업이 성공적으로 완료되면 결과를 반환. | 예외를 던질 수 있는 작업 그룹으로, 작업 도중 에러를 발생시킬 수 있음. | 629 | | **결과 타입** | **Non-throwing Result** (`T`) | **Throwing Result** (`T`) | 630 | | **작업 실패 시 처리** | 작업 실패가 발생하지 않음. | 작업 중 하나라도 에러가 발생하면 그룹 전체가 중단됨. | 631 | | **에러 처리 필요 여부** | 에러 처리가 필요 없음. | 에러 처리(`try`, `catch`) 필요. | 632 | | **사용 예** | - 독립적인 작업 처리.- 작업 실패 가능성이 없는 경우. | - 네트워크 요청, 파일 처리 등 에러 발생 가능성이 있는 작업. | 633 | | **`addTask` 메서드 사용 가능 여부** | 가능 | 가능 | 634 | | **`await` 사용 시** | 단순히 결과를 기다림. | 결과와 함께 에러를 처리해야 함. | 635 | | **에러 전파** | 없음. | 에러가 발생하면 호출자에게 전파. | 636 | 637 | 자식작업을 많이 만들어서 일을 더 잘게 분해하는 게 좋은 것같지만 꼭 그렇지는 않다. 638 | 639 | Task는 자식작업이 모두 완료되는 것을 기다리기 때문에 해당 부모작업의 종료시까지 자식작업들의 메모리를 들고 있게 된다. 640 | 641 | 그 결과를 사용하기 위해서라면 필요하지만 경우에 따라 자식 작업의 결과가 중요하지 않을 수도 있다. 642 | 643 | 이 경우 `with(Throwing)DiscardingTaskGroup()`을 사용하면 된다. 644 | 645 | 이것을 사용하면 자식 작업은 반환되자마자 메모리에서 해제된다. 646 | 647 | # AsyncSequence/Stream 648 | 649 | 위의 코드에서 `for await int in group`를 보았을 것이다. 650 | 651 | 이것은 이번에 새롭게 추가된 `for-await-in` 문법이다. 652 | 653 | 이것은 AsyncSequence를 다루기 위해 등장한 신문법이다. 654 | 655 | 기존 Sequence와 유사하며 여기에 비동기 특성을 부여한 프로토콜이다. 656 | 657 | - Sequence가 제공하던 고차함수들 대부분 사용 가능 658 | 659 | 내가 가진 자료형이 AsyncSequence를 채택하고, next()와 makeAsyncIterator()를 구현하면 사용할 수 있다. 660 | 661 | AsyncStream의 경우 기존 콜백함수나 delegate함수를 async하게 사용할 수 있도록 도와준다. 662 | 663 | ```swift 664 | class QuakeMonitor { 665 | var quakeHandler: (Quake) -> Void 666 | func startMonitoring() 667 | func stopMonitoring() 668 | } 669 | 670 | let quakes = AsyncStream(Quake.self) { continuation in 671 | let monitor = QuakeMonitor() 672 | monitor.quakeHandler = { quake in 673 | continuation.yield(quake) // continuation 인스턴스를 통해서 소통함 674 | } 675 | continuation.onTermination = { @Sendable _ in 676 | monitor.stopMonitoring() 677 | } 678 | monitor.startMonitoring() 679 | } 680 | 681 | let significantQuakes = quakes.filter { quake in 682 | quake.magnitude > 3 683 | } 684 | 685 | for await quake in significantQuakes { 686 | ... 687 | } 688 | 689 | ``` 690 | 691 | continuation 인스턴스를 통해 yield메서드로 값을 반환하기만 하면 사용 가능하다. 692 | 693 | # 협력적 취소 694 | 695 | image 33 696 | 697 | 취소면 취소지 뭔 협력적취소…? 698 | 699 | 말그대로 취소에 “협력”한다는 것이다. 700 | 701 | 우리가 Task에 대해 취소 명령을 날리면 그 즉시 취소(함수 return)되는 것이 **아니다**. 702 | 703 | 다만, Task는 최대한 빨리 취소가 될 수 있도록 “**협력**”하는 것이다. 704 | 705 | 이렇게 하는 이유는 Task와 그 하위 작업들에게 취소가 되었을 경우 어떻게 할 것인지 여지를 주기 위해서다. 706 | 707 | 만약 바로 함수를 종료시키면 취소가 되었을 경우 어떻게 해야하는지를 가이드할 수 없다. 708 | 709 | 그러나 Task가 종료되었을 경우에 어떤 행동을 취할지 개발자에게 여지를 줌으로써 더 유연한 개발이 가능하다. 710 | 711 | ```swift 712 | func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] { 713 | var thumbnails: [String: UIImage] = [:] 714 | for id in ids { 715 | try Task.checkCancellation() 716 | // if Task.isCancelled { break } 717 | thumbnails[id] = try await fetchOneThumbnail(withID: id) 718 | } 719 | return thumbnails 720 | } 721 | 722 | ``` 723 | 724 | 비동기 함수를 작성할 때 Task의 타입 메서드로 `checkCancellation()`과 타입계산속성 `isCancelled`를 사용할 수 있다. 725 | 726 | - `checkCancellation()`: 현 작업이 취소되었는지 확인한 후 취소일 경우 에러를 던짐 727 | - `isCancelled`: 현 작업이 취소되었으면 true, 아니면 false 반환 728 | 729 | 이를 통해 보통 긴 작업을 시작하기 전에 적절히 작업 취소 여부를 확인하여 코드를 작성하면 된다. 730 | 731 | 이러한 방식을 하나도 구현 안하면 작업이 취소되어도 자신의 작업을 계속한다. 732 | 733 | 그러기 때문에 협력적 취소라고 부르는 것이기도 하다. (비협조적이면 취소가 안된다…) 734 | 735 | 위의 Task 타입메서드/속성을 사용하면 현재 자신의 Task를 자동으로 추적한다. 736 | 737 | > 협력적 취소는 구조적 동시성에서 유용하다! 738 | > 739 | - 자식 작업이 오류등을 날리면 다른 작업들도 멈추게 된다. 740 | 741 | # Continuation 742 | 743 | 이렇게 몸에도 좋고 맛도 좋은 SwiftConcurrency지만… 744 | 745 | 기존코드에 당장 적용하기에는 너무 부담이 되는 것도 사실이다. 746 | 747 | 이를 간편하게 해결해주고자 짜잔~ continuation이 있답니다… 748 | 749 | Continuation에는 Checked와 Unsafe 2개가 있다. 750 | 751 | CheckedContinuation의 설명은 다음과 같다. 752 | 753 | - 동기 코드(synchronous)와 비동기(asynchronous) 코드 사이의 인터페이스 제공 754 | - 비동기 상황(어떤 것이 먼저 실행될지 모름, thread를 누가 차지할지 모름)에서도 순서대로 실행될 수 있도록 heap에서 관리함 755 | - 정확성 위반(correctness violations) 기록 756 | - Continuation을 사용할 때, resume은 반드시 1번 불려야한다. (무조건 한번) 757 | - UnsafeContinuation은 이것을 안한다. (나머지 기능은 같음) 758 | 759 | ```swift 760 | // 원래 함수 761 | func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) { 762 | do { 763 | let req = Post.fetchRequest() 764 | req.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] 765 | let asyncRequest = NSAsynchronousFetchRequest(fetchRequest: req) { result in 766 | completion(result.finalResult ?? [], nil) 767 | } 768 | try self.managedObjectContext.execute(asyncRequest) 769 | } catch { 770 | completion([], error) 771 | } 772 | } 773 | 774 | // Async스타일로 변경한 함수 775 | func persistentPosts() async throws -> [Post] { 776 | typealias PostContinuation = CheckedContinuation<[Post], Error> 777 | return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in 778 | self.getPersistentPosts { posts, error in 779 | if let error = error { 780 | continuation.resume(throwing: error) 781 | } else { 782 | continuation.resume(returning: posts) 783 | } 784 | } 785 | } 786 | } 787 | 788 | ``` 789 | 790 | 위의 코드처럼 바꿔줄 수 있다. 791 | 792 | resume을 해야 기존에 await를 했던 부분을 다시 진행한다. 793 | 794 | # Swift Concurrency 795 | 796 | 이렇게 async-await의 등장부터 이를 사용할 수 있는 환경을 제공하는 Task, TaskGroup과 반복을 위한 AsyncSequence, 그리고 Continuation을 통한 기존 API 통합을 살펴보았다. 797 | 798 | 아직 다루지 못한 Sendable, Actor 등은 차치하고, Swift Concurrency가 달성하고 싶었던 목표 중 하나인 가독성을 살펴보았다. 799 | 800 | 그렇다면 드는 의문이 *효율면에서 다른점은 없나?*이다. 801 | 802 | Apple은 그렇다고 한다. 그러면 어떻게 달성되었을까? 803 | 804 | 우리가 여태 하는 이야기가 비동기였다. 이는 Thread와 관련이 깊다. 805 | 806 | Thread의 효율적인 사용이 문제인데, 이는 이전 CS 배울 때 다루었다. 807 | 808 | ⇒ “프로그램이 잘 실행되도록 하는 것이 목표” 809 | 810 | 즉, context-switching을 줄이는 것이 Thread에게 있어서의 과제가 될 것이다. 811 | 812 | 이는 어떻게 반영되었을까? 813 | 814 | image 34 815 | 816 | 기존 GCD환경에서는 위에서처럼 여러 Thread가 CPU자원을 번갈아 가면서 사용되었다. 817 | 818 | 이럴 때 Thread가 많아지면 많아질수록 스케줄링이나 lock과 관련해서 대기 시간이 길어질 수 있었다. 819 | 820 | image 35 821 | 822 | 하지만, SwiftConcurrency에서는 CPU당 Thread를 하나씩 할당한다. 823 | 824 | 그리고 Continuation이라는 객체를 통해서 각각의 실행 맥락을 보존한다. 825 | 826 | 이를 통해 우리가 지불해야하는 비용은 함수 실행 비용 밖에 없다. 827 | 828 | 실제 비동기 함수를 실행할 때를 살펴보자. 829 | 830 | image 36 831 | 832 | 비동기 함수를 실행할 때 우리는 await을 붙이고 호출한다. 833 | 834 | 이때 시스템에게 제어권을 넘기게 되고 함수의 실행이 끝나면 원래 함수를 호출한 쪽으로 돌아와서 resume(계속진행)한다. 835 | 836 | 좀 더 자세히 보자. 837 | 838 | image 37 839 | 840 | Stack과 Heap이 나온다. 841 | 842 | 우리는 함수를 호출하면 변수 등 관련정보를 모아서 Stack에 저장한다는 것을 알고있다. 843 | 844 | 이를 Continuation이라는 객체에 담아서 Heap에 보관했다고 생각하면 된다. 845 | 846 | 그리고 Thread는 현재 Stack만 관리하는 것이다. 847 | 848 | Heap에 Continuation으로 저장하면 Stack에서 제거한다. 849 | 850 | 왜 Heap에 저장하냐? → 여러 스레드에서 공유되기 때문! (어떤 스레드가 실행할지 모르니까) 851 | 852 | > 우선순위 역전 853 | > 854 | 855 | 기존 DispatchQueue의 경우 서로 다른 낮은우선순위와 높은 우선순위가 있을때 높은 우선순위에 우선순위를 일치시켰다. 856 | 857 | 왜냐하면 Queue (선입선출)이기 때문! 858 | 859 | image 38 860 | 861 | 하지만 SwiftConcurrency에서는 Heap에 보관된 Continuation에서 취사선택하면 되므로 우선순위가 높은 것을 먼저 실행 가능하다! 862 | 863 | image 39 864 | 865 | 요롷게! 866 | 867 | # 참고자료 868 | 869 | https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f 870 | 871 | https://engineering.linecorp.com/ko/blog/about-swift-concurrency 872 | 873 | https://developer.apple.com/videos/play/wwdc2021/10254/?source=post_page-----bd7bcf34e26f-------------------------------- 874 | 875 | https://developer.apple.com/videos/play/wwdc2021/10134?time=243&source=post_page-----bd7bcf34e26f-------------------------------- 876 | 877 | https://developer.apple.com/videos/play/wwdc2022/110351 878 | -------------------------------------------------------------------------------- /Week5/Actor, Sendable.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | 3 | - [Actor 등장 배경](#actor-등장-배경) 4 | - [Actor 란?](#actor-란) 5 | - [Actor의 특징(ft. isolated)](#actor의-특징-ft-isolated) 6 | - [Actor isolation](#actor-isolation) 7 | - [Task와 isolation](#task와-isolation) 8 | - [Protocol 준수와 isolation](#protocol-준수와-isolation) 9 | - [Closures와 isolation](#closures와-isolation) 10 | - [Data와 isolation](#data와-isolation) 11 | - [Sendable](#sendable) 12 | - [Sendable 조건](#sendable-조건) 13 | - [Adopting Sendable](#adopting-sendable) 14 | - [@Sendable functions](#sendable-functions) 15 | - [Actor Reentrancy](#actor-reentrancy) 16 | - [Actor Hopping](#actor-hopping) 17 | - [Global Actor](#global-actor) 18 | - [함수에서의 자동 변환](#함수에서의-자동-변환) 19 | - [Protocol 적용시 주의사항](#protocol-적용시-주의사항) 20 | - [Main Actor](#main-actor) 21 | - [Main DispatchQueue](#main-dispatchqueue) 22 | - [Main Actor 내부 Task 생성](#main-actor-내부-task-생성) 23 | - [Main Actor Hopping](#main-actor-hopping) 24 | - [QnA](#qna) 25 | - [참고자료](#참고자료) 26 | 27 | # Actor 등장 배경 28 | 29 | Data race는 동시성 프로그래밍을 정말 어렵게 만드는 오류 중 하나이다. 30 | 31 | Data race는 두 개의 서로 다른 스레드가 동시에 mutable한 데이터에 접근하고 그 중 하나가 데이터를 수정하는 과정에서 발생한다. 32 | 33 | Data race는 발생하기는 굉장히 쉽지만 디버깅하기는 굉장히 까다로운데, 이는 Data race를 유발하는 데이터 접근이 프로그램의 다른 부분에서 이루어질 확률이 높고 이에 따른 비지역적 추론이 필요하기 때문이다.. 34 | 35 | 이러한 Data race를 피할 수 있는 방법은 값 타입을 사용하여 공유 가능한 mutable state를 제거하는 것이다. 특히나 struct와 같은 값 타입에서 let 프로퍼티를 사용하면 완전히 immutable하기 때문에 Data race가 발생할 일이 전혀 없어진다. 36 | 37 | 값 타입을 사용하면 프로그램의 추론이 더 쉬워지고 동시성 프로그래밍을 하면서도 더욱 안전하게 사용할 수 있기 때문에 Swift에서는 값 타입의 사용을 권장하고 있다. 38 | 39 | 하지만 저대로 우리가 원하는 바를 다 ~ 이룰 수 있다면 Actor가 나오지 않았겠죠? 40 | 41 | 결국 우리는 Data Race가 일어나지 않으면서 공유 가능한 mutable state를 필요로한다. 42 | 43 | 기존의 Swift엔 저런 것.. 존재하지 않았다.. 44 | 45 | ~~만들어 줘.~~ 46 | 47 | 그래서 Apple에서 만들어준게 바로 **Actor**이다. 48 | 49 | # Actor 란? 50 | 51 | actor는 Swift의 새로운 타입으로 struct, enum, class와 사용법이 동일하다 52 | 53 | ```swift 54 | actor Counter { 55 | var value: Int = 0 56 | 57 | func increment() -> Int { 58 | value += 1 59 | return value 60 | } 61 | } 62 | ``` 63 | 64 | actor의 특징은 아래와 같다. 65 | 66 | - class와 같은 **reference type** 67 | 68 | → actor의 목적이 shared mutable state의 표현이기 때문.. 69 | 70 | - class와는 다르게 **상속 불가능** 71 | - Property, Method, Initializer, subscripts등 모두 가질 수 있고 protocol을 채택할 수도, extension을 써서 확장을 할 수도 있음 72 | 73 | 추가로 Actor에서 가장 주요하게 class와 구별되는 특성은 바로 **인스턴스 데이터를 나머지 프로그램으로부터 분리**하고 해당 **데이터에 대한 동기화된 접근을 보장**한다는 것이다. 74 | 75 | 이에 관해서는 아래서 자세히 다뤄보자. 76 | 77 | # Actor의 특징 (ft. isolated..) 78 | 79 |

80 | 81 |

82 | 83 | **Actor**는 **공유 가능한 mutable state의 동기화**를 위한 **동기화 메커니즘**을 제공한다. 84 | 85 | Actor의 동기화 메커니즘은 해당 actor의 상태로 동시에 다른 두 개의 코드가 접근하지 않는다는 것을 보장한다. 이러한 특성은 `Locks`나 `Serial dispatch queue`와 같은 **상호 배제 속성(mutual exclusion property)** 을 언어 수준에서 제공한다. 86 | 87 | 또한 **컴파일러 수준**에서 **데이터 격리**를 제공한다. 이를 통해 가변 상태의 데이터를 보호할 수 있게 된다. 88 | 89 | Actor의 기본 원리는 저장된 인스턴스 속성에 `self`만 접근을 허용하는 것이다. 90 | 91 | Actor는 자신만의 상태를 가지고 있으며 그 상태는 나머지 프로그램으로부터 독립적인 상태이다. 그리고 이러한 Actor의 상태는 Actor 자기 자신을 통해서만 접근할 수 있다. 92 | 93 | 예제를 통해 살펴보자. 94 | 95 | ```swift 96 | actor BankAccount { 97 | let accountNumber: Int 98 | var balance: Double 99 | } 100 | 101 | extension BankAccount { 102 | enum BankError: Error { 103 | case insufficientFunds 104 | } 105 | 106 | func transfer(amount: Double, to other: BankAccount) throws { 107 | if amount > balance { 108 | throw BankError.insufficientFunds 109 | } 110 | 111 | print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)") 112 | 113 | balance = balance - amount 114 | other.balance = other.balance + amount // error: actor-isolated property 'balance' can only be referenced on 'self' 115 | } 116 | } 117 | ``` 118 | 119 | 위 코드에서 중요한 부분은 actor isolated가 같은 타입의 서로 다른 인스턴스 사이에도 적용된다는 점이다. 120 | 121 | 위 메서드에서 `balance = balance - amount`은 `self` 이기 때문에 내부에서 격리된 상태에 접근하는 것으로 취급된다. 따라서 별다른 문제가 되지 않는다. 122 | 123 | 그러나 `other.balance = other.balance + amount` 부분은 `other` 라는 매개변수에 접근하는 것이기 때문에 `self`의 ‘**외부**’에 해당한다. 124 | 125 | 따라서 저 부분에서는 컴파일 에러가 발생한다. 126 | 127 | 근데 `other.accountNumber`를 사용하는 print문에서는 에러가 발생하지 않는다. 이유가 무엇일까?? 128 | 129 | 이는 밑에서 설명할 `Sendable`과 관련이 있는데, `accountNumber` 프로퍼티는 불변 프로퍼티이다. 따라서 정의상 Data Race가 일어나지 않기 때문에 다른 actor 객체의 프로퍼티에 직접 접근하더라도 에러가 발생하지 않는 것이다. 130 | 131 | 지금은 조금 이해가 안되더라도 쭉쭉 읽다보면.. 이해가 될 것…임.. 132 | 133 | ## Actor isolation 134 | 135 | actor isolation은 actor에서 아주아주아주 중요한 개념 중 하나이다. 136 | 137 |

138 | 별이 5개 .ᐟ.ᐟ 139 |

140 | 141 | actor에서 선언된 대부분의 요소들(property, method 등..)은 특별히 명시하지 않는 이상 **actor에 격리**되어있다. 142 | 143 | 아 ~~ 아까부터 격리 격리 하는데 격리가 뭔데요;; 144 | 145 | 격리(isolated)되었다는 것은 쉽게말해 **외부에서 직접적으로 조작할 수 없고 `self`을 통해서만 조작할 수 있다는 것**을 말한다. 146 | 147 | 이를 **actor의 경계선 안에 있다**고 표현한다. 148 | 149 | 이러한 actor isolation은 actor 타입의 근간이 되는 동작이다. 여기서부턴 actor isolation이 어떻게 Swift의 다른 언어적 특성들과 상호작용 하는지 코드를 통해 알아보자. 150 | 151 | ### Task와 isolation 152 | 153 | Task와 Actor 간의 격리는 매우 중요한 개념이다. 154 | 155 | 우리가 actor를 동작시키려면 특정 Task를 사용해서 actor에 접근해야하는데, 이때 actor의 상태 격리를 Task 내부에서도 유지해야한다. 156 | 157 | 즉, Task 내부에서의 상태와 actor 자체의 상태가 격리되어야 하는데, 이를 어떻게 구분할까? 158 | 159 | **actor의 격리**는 **해당 코드가 존재하는 context에 따라 결정**된다. 이렇게 얘기하면 이해가 안될테니 아래 예시를 통해 이해해보자. 160 | 161 | 162 | 163 | 일단 **actor의 프로퍼티와 메서드**는 **해당 actor로 격리**된다. 164 | 165 | `reduce`로 전달된 틀로저와 같이 **Sendable하지 않은 클로저**의 경우 **actor-isolated context 내부에 있을 때 actor-isolated** 된다. 166 | 167 | **Task initializer** 또한 **context에서 actor isolation을 상속**하므로 생성된 Task는 **처음 시작된 actor와 동일한 actor에 의해 관리**된다. 168 | 169 | 반면, **detached된 Task**의 경우 **actor isolation을 상속하지 않음으로 actor로 격리되지 않는다**. 따라서 해당 작업은 actor 외부에 존재하는 것으로 간주되어 actor에 격리된 프로퍼티 혹은 메서드에 접근하기 위해 `await` 키워드를 사용해야한다. 170 | 171 | ### Protocol 준수와 isolation 172 | 173 | 아래와 같은 예시가 있다고 하자. 174 | 175 | ```swift 176 | actor LibraryAccount { 177 | let idNumber: Int 178 | var booksOnLoan: [Book] = [] 179 | } 180 | 181 | extension LibraryAccount: Equatable { 182 | static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool { 183 | lhs.idNumber == rhs.idNumber 184 | } 185 | } 186 | ``` 187 | 188 | `LibraryAccount` actor는 `Equatable` 프로토콜을 채택하고있고, static equality(==) 메서드는 두 개의 library account를 ID를 기준으로 비교하고 있다. 189 | 190 | 이 메서드는 static이며, `self` 인스턴스가 없으므로 actor에게서 독립되어있지 않다. 191 | 192 | 대신 두 개의 actor 타입의 파라미터를 가지고 있고, 이 static 메서드는 그 두 개의 바깥에 존재하며 구현체는 **각각의 actor의 불변 상태에만 접근**하고 있으므로 **에러가 발생하지 않는다.** 193 | 194 | 그럼 위 예시를 확장시켜서 `LibraryAccount`가 `Hashable`을 준수하고 있다면 어떻게 될지 알아보자. 195 | 196 | ```swift 197 | actor LibraryAccount { ... } 198 | 199 | extension LibraryAccount: Hashable { 200 | func hash(into hasher: inout Hasher) { 201 | hasher.combine(idNumber) // Compile Error ! 202 | } 203 | } 204 | ``` 205 | 206 | 이번 코드에선 컴파일 에러가 발생한다 .ᐟ.ᐟ 207 | 208 | 아니.. 위에 함수랑 동일하게 불변 상태에만 접근하고 있는데 왜 에러가…? 209 | 210 | 위와 같은 방식으로 `Hashable`을 준수한다는 것은 `hash(into:)` 메서드가 액터 외부에서 호출될 수 있다는 것을 의미한다. 211 | 212 | 하지만 `hash(into:)`는 비동기 함수가 아니기 때문에 actor isolation을 유지할 방법이 없다. 213 | 214 | 그럼 이거 어케고침 ?! actor는 Hashable 채택 못함 ?! 215 | 216 | 노노 ~ `nonisolated` 키워드를 붙이면 해결된다. 217 | 218 | ```swift 219 | actor LibraryAccount { ... } 220 | 221 | extension LibraryAccount: Hashable { 222 | nonisolated func hash(into hasher: inout Hasher) { 223 | hasher.combine(idNumber) // Compile OK ! 224 | } 225 | } 226 | ``` 227 | 228 | **`nonisolated`** 키워드는 **해당 메서드가 actor에 내부에 명시되어 있더라도, actor 외부에 있는 것처럼 처리됨**을 의미한다. 229 | 230 | 대신 `nonisolated` 메서드는 actor 외부에 있는 것으로 처리되기 때문에 actor의 mutable한 상태에는 접근할 수 없다. 231 | 232 | ### Closures와 isolation 233 | 234 | 위에선 프로토콜을 준수하는 과정에서 actor isolation과 protocol간의 상호작용에 대해 알아봤다. 235 | 236 | 이번엔 closure다. 237 | 238 | ```swift 239 | actor LibraryAccount { 240 | let idNumber: Int = 0 241 | var booksOnLoan: [Book] = [] 242 | } 243 | 244 | extension LibraryAccount { 245 | func readSome(_ book: Book) -> Int { ... } 246 | 247 | func read() -> Int { 248 | booksOnLoan.reduce(0) { prev, book in 249 | readSome(book) 250 | } 251 | } 252 | } 253 | ``` 254 | 255 | `read` 함수 내부에서 `readSome` 메서드를 호출할 때, `await` 키워드를 붙이지 않고 있다. 이는 **actor isolated한 `read` 함수 내부에 형성된 클로저 또한 actor isolated하기 때문**이다. 256 | 257 | 그럼 아래와 같은 예제를 보자. 258 | 259 | ```swift 260 | extension LibraryAccount { 261 | ... 262 | 263 | func readLater() { 264 | Task.detached { 265 | await self.read() 266 | } 267 | } 268 | } 269 | ``` 270 | 271 | `readLater` 라는 메서드는 메서드 내부에서 Task 블럭을 생성하고있다. 272 | 273 | 그리고 이 task 블럭은 `detached`이기 때문에 actor가 하는 다른 일들과 동시적으로 실행된다. 그러므로 이 클로저는 actor nonisolated 한 상태라고 할 수 있다. 274 | 275 | 즉, 위와 같은 detached Task 내부에서 `read` 메서드를 호출하려면 `await` 키워드를 통해 `read` 메서드가 비동기적으로 실행되게끔 해줘야한다. 276 | 277 | ### Data와 isolation 278 | 279 | 위 예제 코드를 보면 `booksOnLoan` 에 들어간 `Book` 이 무슨 타입인지에 관해서는 이야기하지 않았다. 280 | 281 | `Book`을 struct 타입이라고 가정해보자. 282 | 283 | 284 | 285 | 그럼 위 상황과 같이 책의 title과 같은 프로퍼티를 바꿔주더라도 actor에는 아무런 영향을 끼치지 않을 수 있다. 286 | 287 | 하지만 만약 `Book`이 class 타입이라면? 288 | 289 | 290 | 291 | 그럼 이제 book의 title을 업데이트 해줬을 때, 참조값 자체가 바뀜으로 data race가가 발생할 수 있게 된다. 292 | 293 | 값 타임이나 actor는 동시성에서 안전하게 사용할 수 있지만, class 타입은 여전히 문제가 된다. 그렇다고 여태까지 class로 쓰던 모든 타입을 actor로 변경해? 그건 에바잖아요 .ᐟ.ᐟ 294 | 295 | 그래서 또 Apple에서 동시적으로 사용해도 안전하다는 것을 명시해줄 수 있는 **`Sendable`** 이라는 친구를 만들어줬다. 296 | 297 | ## Sendable 298 | 299 | 우리는 격리만 하기 위해서 actor를 쓰는게 아니다. 우리가 actor를 사용하는 목적은 shared mutable state의 사용임을 잊지 말아야한다. 300 | 301 | 즉, **actor의 경계를 넘어(cross-actor reference)** actor 내부 요소를 사용하기 위해선 그 격리된 경계를 넘어야한다. 302 | 303 | 여기엔 2가지 방법이 있다. 304 | 305 | 1. 불변 상태 요소 사용 306 | 307 | → actor가 정의된 것과 동일한 모듈의 어느 곳에서든 불변 상태에 대한 교차 actor 참조가 허용되는 이유는 한 번 초기화되면 해당 상태가 actor의 내부 또는 외부에서 수정할 수 없으므로 **정의상 데이터 경합이 존재하지 않기 때문**이다. 308 | 309 | 2. 비동기 함수 호출로 수행 310 | 311 | 비동기 함수를 호출하면 actor가 해당 작업을 실행하도록 요청하는 ‘메시지’로 변환한다. 그렇게 변환된 메시지는 actor에 의해 한 번에 하나씩만 처리된다. 312 | 313 | DispatchQueue와 다른 점은 이 작업이 queue처럼 FIFO 방식으로 진행되지 않는다는 점이다. 314 | 315 | 즉, **메시지의 실행 순서 ≠ 메시지의 도착 순서**이다. 316 | 317 | 318 | 여기서 또 주의해야 할 사항이 있다. 319 | 320 | 바로, 2번 방법을 통해 값을 외부로 넘기기 위해서는 **경계를 넘겨 보낼 수 있는 값**이어야 한다는 것이다. 321 | 322 | 이러한 타입을 **Sendable** 타입이라고 한다. 323 | 324 | 즉, Sendable은 **격리된 도메인(actor)의 경계를 넘길 수 있는 자격 조건**이다. 325 | 326 | ### Sendable 조건 327 | 328 | Sendable가 될 수 있는 것은 아래와 같은 것들이 해당된다. 329 | 330 | - Value types 331 | - Actor types 332 | - Immutable Classes (완전한 불변상태의 클래스) 333 | - Internally-synchronized Class (내부적으로 동기처리가 된 클래스) 334 | - 개발자가 직접 mutex 등을 이용하여 관리하는 경우를 말한다. 335 | - 이 경우 컴파일러는 이러한 상황을 모르기 때문에 `@unchecked Sendable` 키워드를 붙여줘야한다. 336 | - `@Sendable` function types 337 | 338 | ### Adopting Sendable 339 | 340 | `Sendable`은 프로토콜이라 다른 프로토콜들과 같이 그냥 채택해주기면 하면된다. 341 | 342 | struct의 경우 해당 struct 내부에 모든 저장 프로퍼티들이 `Sendable` 타입이면 그 struct 또한 `Sendable`을 채택할 수 있다. 343 | 344 | ```swift 345 | struct Book: Sendable { 346 | var title: String 347 | var authors: [Author] // 만약 Author이 non-Sendable이면 컴파일 에러 ! 348 | } 349 | ``` 350 | 351 | 제네릭에서도 `Sendable`을 쓸 수 있는데 아래 예제와 같이 여러 제네릭 타입이 있다면 그 모든 타입들이 모두 `Sendable`일 때, 상위 제네릭 타입도 `Sendable` 타입이 될 수 있다. 352 | 353 | ```swift 354 | struct Pair { 355 | var first: T 356 | var second: U 357 | } 358 | 359 | extension Pair: Sendable where T: Sendable, U: Sendable { ... } 360 | ``` 361 | 362 | ### `@Sendable` functions 363 | 364 | 프로퍼티 뿐만 아니라 함수도 Sendable 이 될 수 있다. 365 | 366 | 함수가 Sendable이 되기 위해선 아래와 같은 조건을 만족해야한다. 367 | 368 | - mutable 캡쳐가 없어야 함 369 | - 캡쳐가 Sendable 타입이어야 함 370 | - 동기적인 Sendable 클로저는 actor-isolated할 수 없음 371 | 372 | > Sendable이 붙었다는 것은 **actor 경계를 넘나들 수 있음을 뜻**하고 동기적으로 작동한다는 것은 함수가 **언제 어디서 호출되어도 바로 실행 가능**하다는 뜻이다. 따라서 **actor만의 분리된 상태를 가지는 값이 있을 수 없기에 actor-isolated 할 수 없다.** 373 | > 374 | 375 | Sendable 함수 타입은 **어디서 동시 실행이 일어날 수 있는지 나타내주고**, 이를 통해 **data race를 예방**할 수 있다. 376 | 377 | ## Actor Reentrancy 378 | 379 | **actor는 한 번에 하나의 Task 실행만 허용**하는 방식으로 Data Race 문제를 해결해왔다. 비동기 함수의 경우 오래걸리는 작업을 하며 다른 작업이 actor에 진입하는 것을 계속해서 막는 것은 비효율적이다. 380 | 381 | 따라서 **잠재적 중단지점인 await에서 actor 점유를 내려놓고 다른 task가 actor에 진입할 수 있게** 하며, 이것을 **actor reentrancy(재진입)** 라고 한다. 382 | 383 | 하지만 여기서 actor 내부의 원자성을 보존하기 힘들어진다는 문제점이 발생한다. 384 | 385 | ```swift 386 | actor DecisionMaker { 387 | let friend: Friend 388 | 389 | // actor-isolated opinion 390 | var opinion: Decision = .noIdea 391 | 392 | func thinkOfGoodIdea() async -> Decision { 393 | opinion = .goodIdea // <1> 394 | await friend.tell(opinion, heldBy: self) // <2> 395 | return opinion // 🤨 // <3> 396 | } 397 | 398 | func thinkOfBadIdea() async -> Decision { 399 | opinion = .badIdea // <4> 400 | await friend.tell(opinion, heldBy: self) // <5> 401 | return opinion // 🤨 // <6> 402 | } 403 | } 404 | 405 | let goodThink = Task.detached { await decisionMaker.thinkOfGoodIdea() } // runs async 406 | let badThink = Task.detached { await decisionMaker.thinkOfBadIdea() } // runs async 407 | 408 | let shouldBeGood = await goodThink.get() 409 | let shouldBeBad = await badThink.get() 410 | 411 | await shouldBeGood // could be .goodIdea or .badIdea ☠️ 412 | await shouldBeBad 413 | ``` 414 | 415 | 위 예시 코드를 보면, `goodThink`와 `badThink` 각각의 task가 비동기적으로 실행됨을 알 수 있다. 416 | 417 | `thinkOfGoodIdea` 와 `thinkOfBadIdea` 각각의 메서드 내부의 코드는 분명 순차적으로 진행되지만 메서드 내부에서 `await` 키워드를 만나면서 함수 실행 중 중단이 된다. 그럼 이후 actor 내부 프로퍼티인 `opinion`값을 반환할 때엔 이 다른 값으로 바뀔 수 도 있게 되는 것이다. 418 | 419 | 즉, actor가 Data Race는 해결해 줄 수 있더라도 **Race Condition과 원자성을 보장해주지 않는다**는 뜻이다. 420 | 421 | 이러한 점 때문에 actor를 사용할 때에는 아래와 같은 점들을 주의해야한다. 422 | 423 | - 상태의 변경은 동기 코드에서 실행시킬 것 424 | - 중단점에서 actor의 상태가 변화할 수 있음을 인지하고 있을 것 425 | - `await` 키워드 이후의 상태를 생각할 것 426 | 427 | 여기까지 보면 재진입이 단점만 발생시키는 actor의 기능인 것 같은데, 재진입 덕분에 가지는 장점도 있다. 428 | 429 | 바로 GCD의 단점 중 하나였던 priority inversion 문제를 해결해준다는 것이다. 430 | 431 | 432 | 433 | GCD의 serial queue의 경우 엄격한 FIFO 방식을 따른다. 434 | 435 | 이러한 특성 때문에 위와 같은 상황에서 priority가 더 높은 B 작업을 수행하기 위해서는 priority가 더 낮은 1,2,3,4,5의 작업을 먼저 수행해야하는데, 이를 priority inversion이라고 한다. 436 | 437 | 438 | 439 | 위 사진에서 Database, Sports feed 모두 actor를 나타낸다. 440 | 441 | 위와 같은 상황에서 Sports feed actor가 Database actor를 호출하면 uncontended 상태이기 때문에 Database actor에 pending된 작업이 있더라도 Database actor로 hop할 수 있다. (Hopping에 대해서는 아래서 다룰 거임요 ^0^) 442 | 443 | 그리고 Sports feed의 호출에 의한 `database.save` 작업을 수행하기 위해 이 전의 작업과는 별개의 work item(`D2`)을 생성하고 그걸 실행한다. 444 | 445 | 이런식으로 actor reentrancy는 엄격한 FIFO 방식으로 작업이 진행되지 않고 나중에 생긴 작업(`D2`)이 먼저 시작될 수 있는 것을 알 수 있다. 446 | 447 | 추가로 Apple이 actor reentrancy를 만들게 된 이유를 생각해보면 Dead Lock 발생 가능성 제거하기 위해서라는 이유가 존재할 것 같다. (여긴 약간 뇌피셜 섞임 주의) 448 | 449 | Dead Lock은 런타임에서만 검증이 가능한데 이 점이 Swift Concurrency와는 방향성이 맞지 않기 때문에 재진입이라는 기능을 도입하게 된 게 아닐까… 싶은.. ㅎㅅㅎ 450 | 451 | ## Actor Hopping 452 | 453 | actor 내부에서 다른 actor의 메서드를 호출하면 어떻게 될까? 454 | 455 | actor의 동작은 **cooperative thread pool에서 수행**되는데, **한 actor에서 다른 actor로 전환하는 것**을 **Actor Hopping**이라고 한다. 456 | 457 | 458 | 459 | 위와 같은 actor들이 있을 때 스레드 변화를 살펴보며 이해해보자. 460 | 461 | 1. Sports feed actor가 협력 스레드(cooperative thread) 1번에서 동작하다가 Database actor의 `save` 메서드를 호출했다. 462 | 463 | 464 | 465 | 2. 현재 Database actor를 아무도 사용하지 않으므로 경쟁이 없는 상태다. (이런 상태를 NonContention 상태라고 한다.) 따라서 Sports feed는 곧장 Database actor로 이동(hopping) 할 수 있다. 그리고 Sport feed는 `await database.save`에 의해 중단 상태가 된다. 466 | 3. Sports feed가 동작하던 스레드는 중단점에 의해 다른 작업이 올 수 있게 된다. 따라서 아래 그림처럼 Database actor가 해당 스레드에서 작업을 하게된다. 467 | 468 | 469 | 470 | 4. 이 상황에서 Weather feed actor가 다른 스레드에서 동작하다가 Database actor를 사용하려 한다면 Database actor는 D2 작업을 생성한다. 하지만 Database actor는 `D1` 작업을 실행하고 있기 때문에 `D2` 작업은 보류 상태가 된다. 471 | 472 | 473 | 474 | 5. Weather feed actor가 동작하는 도중 중단점을 만나면 해당 스레드에는 다른 작업이 올 수 있다. 아래 그림과 같이 Health feed actor의 작업이 왔다고 가정해보자. 475 | 476 | 477 | 478 | 6. 시간이 지나 `D1` 작업이 종료되면 보류중인 `D2` 작업, 기존에 멈춘 `S1` 작업, `W1` 작업 중 우선순위가 높은 순서대로 작업을 계속해서 하게된다. 479 | 480 | 481 | 482 | 483 | 위 과정들을 보면 알 수 있듯 actor hopping 이 가지는 두 가지 특징이 있다. 484 | 485 | - Non-blocking Thread (스레드를 block하지 않음) 486 | - No More Thread (추가적인 스레드를 필요로하지 않음) 487 | 488 | ~~개인적으로 continuation과 비슷하다…는 느낌은 받은 부분.. 움움..~~ 489 | 490 | # Global Actor 491 | 492 | Global Actor(전역 actor)는 위 actor의 제한 사항을 좀 더 풀어주기 위해 만들어졌다. 493 | 494 | actor가 격리 상태를 제공하여 데이터 경쟁을 피하게 해준다는 취지는 좋다. 하지만 만약 격리 상태가 필요한 코드 부분들이 여기저기 흩어져있으면 어떡할까? 495 | 496 | 예를 들어, UI는 MainActor에서 동작해야 하는데, 모든 class, property, function, delegate 등을 `extension MainActor { }`로 감싸는 것은 비현실적이다. 497 | 498 | 이것을 해결해줄 수 있는 것이 바로 Global Actor이다. 499 | 500 | 먼저 간단한 global actor의 사용법을 알아보자. 501 | 502 | ```swift 503 | @globalActor 504 | public struct SomeGlobalActor { 505 | public actor MyActor { } 506 | public static let shared = MyActor() 507 | } 508 | ``` 509 | 510 | 위 코드와 같이 `@globalActor` 키워드를 추가함으로써 custom global actor를 만들 수 있다. 이 `@globalActor` 키워드는 어떤 타입이든 붙을 수 있다. 511 | 512 | 어디서 많이 보던 문법 아닌가? 513 | 514 | ```swift 515 | @MainActor 516 | class CustomViewController: UIViewController { ... } 517 | ``` 518 | 519 | 맞다! 아래서 설명할 것이지만, Main Actor가 바로 이 global actor를 활용하여 만들어진 actor이다. 520 | 521 | ```swift 522 | @globalActor 523 | public actor MainActor { 524 | public static let shared = MainActor(...) 525 | } 526 | ``` 527 | 528 | ### 함수에서의 자동 변환 529 | 530 | 만약 특정 global actor를 한정하여 선언한 곳에 아무 actor 제한이 없는 함수를 넣으면 자동으로 global actor에서 실행되도록 가정한다. 531 | 532 | ```swift 533 | var callback: @MainActor (Int) -> Void 534 | 535 | func acceptInt(_: Int) { } // 어떠한 actor 제한도 없음 536 | 537 | callback = acceptInt // @MainActor (Int) -> Void로 변환되어 돌아감 538 | ``` 539 | 540 | 하지만 역으로 적용시키면 에러가 발생한다. 541 | 542 | ```swift 543 | let fn3: (Int) -> Void = callback // error: removed global actor `MainActor` from function type 544 | ``` 545 | 546 | 547 | 548 | ### Protocol 적용시 주의사항 549 | 550 | ```swift 551 | protocol P { 552 | @MainActor func f() 553 | } 554 | 555 | struct X { } 556 | 557 | extension X: P { 558 | func f() { } // 암시적 @MainActor 559 | } 560 | 561 | struct Y: P { } 562 | 563 | extension Y { 564 | func f() { } // 암시적 @MainActor이 되지 않는다. 565 | // 왜냐하면 Protocol P의 준수와 분리된 extension에 정의되어있기 때문이다. 566 | // 이 경우 error가 나지는 않고 MainActor로 돌아가지 않는다. 567 | } 568 | ``` 569 | 570 | 특정 프로토콜이 global actor를 준수하도록 요구하면 해당 프로토콜을 준수하는 scope(extension?)에서 구현해야 요구사항을 만족할 수 있다. 571 | 572 | # Main Actor 573 | 574 | main actor는 단어부터 대놓고 알려주고 있는대로 **메인 스레드를 나타내는 특별한 Global Actor**를 **Main Actor**라고 한다. 575 | 576 | 기본 actor 코드는 백그라운드 스레드에서 실행되지만 Main Actor로 격리된 코드는 무조건 메인 스레드에서 실행된다. 577 | 578 | ## Main DispatchQueue 579 | 580 | main actor는 main GCD를 통해 모든 동기화를 수행한다. 581 | 582 | actor의 executor 관점에서 main actor의 executor는 main GCD에 해당한다. 583 | 584 | ```swift 585 | DispatchQueue.main.async {} 586 | await MainActor.run {} 587 | ``` 588 | 589 | 따라서 **`MainActor`는 `DispatchQueue.main`을 사용해 교체**할 수 있고, 반대로 **`DispatchQueue.main.async` 작업은 `MainActor.run`으로 대체**할 수 있다. 590 | 591 | MainActor의 `run` 메서드의 정의는 아래와 같다. 592 | 593 | ```swift 594 | static func run( 595 | resultType: T. Type = T. self, 596 | body: @MainActor () throws -> T 597 | ) async rethrows -> T where T: Sendable 598 | ``` 599 | 600 | `run`은 비동기함수로 구현되어있는데, 이는 메인 스레드에서 동작할 수 있을 때까지 기다려야 할 수도 있기 때문에 중단점을 두어 해당 스레드에서 다른 작업이 실행될 수 있도록 하기 위함이다. 601 | 602 | 메인 스레드에서 한꺼번에 많은 작업이 이뤄지길 원하는 경우엔 아래와 같이 묶어서 호출해야 일시중단 없이 한 번에 실행 가능하다. 603 | 604 | ```swift 605 | await MainActor.run { 606 | // UI 관련 코드 1 607 | // UI 관련 코드 2 608 | } 609 | ``` 610 | 611 | ## Main Actor 내부 Task 생성 612 | 613 | 614 | 615 | 616 | 617 | **actor 내부에서 Task를 생성**하는 시점에 도달하면 **Swift는 원래 scope과 동일한 actor에서 해당 작업이 실행되도록 스케줄링**한다. 618 | 619 | 따라서 위 코드에서 노란 네모 Task는 Main Actor에서 실행 될 것이다. 620 | 621 | 622 | 623 | 시스템은 호출자에게 즉시 반환하고, Task는 추후 메인 스레드에서 실행될 것이다. 624 | 625 | Main Actor로 격리된 VC에서 Task를 생성해도, Task는 주변 컨텍스트를 이어가기 때문에 Task 내부 작업들은 메인 스레드에서 동작하게 된다. 626 | 627 | 그럼 만약 Task 내부에서 비동기 메서드를 호출하면 어떻게 될까? 628 | 629 | 630 | 631 | 위와 같은 상황이 있다고 해보자. 632 | 633 | 위 코드에서 `download(url:)` 메서드도 과연 메인 스레드에서 동작할까? 634 | 635 | 이에 대한 정답은 해당 **비동기 메서드가 어디에 격리되어 있느냐**에 따라 다르다는 것이다. 636 | 637 | 위 상황을 그림으로 살펴보자. 638 | 639 | 640 | 641 | main actor에서 Task를 사용하면 해당 작업도 main actor에 격리된다. 그러나 await 키워드를 만나면 메인 스레드 제어권을 시스템에게 넘긴다. 642 | 643 | 644 | 645 | 시스템에 의해 `download` 메서드가 실행될 장소가 정해지는데, 이 메서드가 struct의 메서드라고 가정해보자. 646 | 647 | 그럼 어떠한 actor에도 격리되어 있지 않기 때문에 thread pool의 임의의 스레드에서 해당 비동기 메서드 작업이 실행될 수 있게 된다. 648 | 649 | ## Main Actor Hopping 650 | 651 | 652 | 653 | 메인 스레드는 cooperative thread pool과 격리되어있다. 654 | 655 | 이는 즉, 아래 사진과 같이 **Main Actor와 다른 기본 actor들 간의 hopping이 일어날 때에는 Thread Context Switching이 발생**함을 의미한다. 656 | 657 | 658 | 659 | 그래서 코드를 아래와 같이 잘못 작성하면.. 660 | 661 | 662 | 663 | 이렇게 스레드간 컨텍스트 스위칭이 자주 발생하여 성능저하가 일어날 수 있다. 664 | 665 | 그러므로 위와 같은 상황에서는 Main Actor에서 할 작업을 일괄로 처리할 수 있도록 재구조화를 해야한다. 666 | 667 | 668 | 669 | 이렇게 `Article`을 불러오는 작업(백그라운드 작업)을 한 번에 수행하고 UI 업데이트 작업(메인 스레드 작업)을 한 번에 수행하는 방식으로 바꿈으로써 컨텍스트 스위칭 비용을 줄일 수 있다. 670 | 671 | # QnA 672 | 673 | - `Sendable`을 채택하면 내부 데이터를 안전하게 보장할 수 있을까? 674 | - Actor는 클래스와 어떻게 다를까? 675 | - Actor는 왜 필요할까? 676 | 677 | # 참고자료 678 | 679 | https://developer.apple.com/news/?id=o140tv24 680 | 681 | https://developer.apple.com/kr/videos/play/wwdc2021/10133/ 682 | 683 | https://developer.apple.com/videos/play/wwdc2021/10254 684 | 685 | https://developer.apple.com/videos/play/wwdc2022/110350 686 | 687 | https://developer.apple.com/videos/play/wwdc2022/110356 688 | 689 | https://developer.apple.com/videos/play/wwdc2022/110351 690 | 691 | https://developer.apple.com/videos/play/wwdc2023/10170 692 | 693 | https://developer.apple.com/documentation/Swift/Copyable 694 | 695 | https://forums.swift.org/t/a-roadmap-for-improving-swift-performance-predictability-arc-improvements-and-ownership-control/54206 696 | 697 | https://github.com/swiftlang/swift-evolution/blob/main/proposals/0306-actors.md 698 | 699 | https://github.com/swiftlang/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md 700 | 701 | https://zeddios.tistory.com/1290 702 | 703 | https://sujinnaljin.medium.com/swift-actor-%EB%BF%8C%EC%8B%9C%EA%B8%B0-249aee2b732d -------------------------------------------------------------------------------- /Week6/Copyable, NonCopyable.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | 3 | - [깊은 복사와 얕은 복사](#깊은-복사와-얕은-복사) 4 | - [깊은 복사](#깊은-복사) 5 | - [얕은 복사](#얕은-복사) 6 | - [클래스의 깊은 복사](#클래스의-깊은-복사) 7 | - [Copyable](#copyable) 8 | - [등장 배경](#등장-배경) 9 | - [Copyable이란](#copyable이란) 10 | - [Non-Copyable (aka. Move-Only Type)](#non-copyable-aka-move-only-type) 11 | - [Non-Copyable 사용하기](#non-copyable-사용하기) 12 | - [소유권 (Ownership)](#소유권-ownership) 13 | - [1. consume](#1-consume) 14 | - [2. borrow](#2-borrow) 15 | - [3. inout (mutating)](#3-inout-mutating) 16 | - [NonCopyable의 deinit](#noncopyable의-deinit) 17 | - [프로토콜과 제네릭 활용](#프로토콜과-제네릭-활용) 18 | - [조건부 Copyable](#조건부-copyable) 19 | - [동시성과 관계](#동시성과-관계) 20 | - [참고자료](#참고자료) 21 | 22 | # 깊은 복사와 얕은 복사 23 | 24 | Copyable에 대해 본격적으로 알아보기 전에, Swift에서의 Copy라는 행위에 대해 다시 한번 짚어보겠다. 25 | 26 | ## 깊은 복사 27 | 28 | 29 | 30 | 기본적으로 값타입인 struct, enum은 깊은 복사를 이용한다. 31 | 32 | 즉, 내부 데이터까지 값이 복사되된다는 뜻이다. 33 | 34 | ⇒ 독립적인 각자의 인스턴스 소유 35 | 36 | ## 얕은 복사 37 | 38 | 39 | 40 | 참조 타입인 class, actor에서 동작하는 방식이다. 41 | 42 | 즉, 값이 복사 될 때 참조값만 복사하고 가리키는 인스턴스는 동일하다는 의미이다. 43 | 44 | ex) `let player2 = player1` 이라는 코드에서 player2는 player1의 참조만 갖게 되고 이 둘의 인스턴스는 공유된다. 45 | 46 | ## 클래스의 깊은 복사 47 | 48 | 대다수가 이미 알고있다싶이 값 타입을 복사하면 값 자체가 복사가 되고 참조 타입을 복사하면 참조가 복사된다. 49 | 50 | 하지만 이러한 참조 타입의 복사도 깊은 복사 방식을 사용하여 값 타입의 복사와 동일하게 동작하도록 구현 가능하다. 51 | 52 | 아래 코드와 같은 `Icon`과 `PlayerClass`가 있다고 해보자. 53 | 54 | ```swift 55 | struct Icon { 56 | var icon: String 57 | init(_ icon: String) { self.icon = icon } 58 | init(from other: Icon) { self.icon = other.icon } 59 | } 60 | 61 | class PlayerClass { 62 | var data: Icon 63 | init(_ icon: String) { self.data = Icon(icon) } 64 | 65 | init(from other: PlayerClass) { 66 | self.data = Icon(from: other.data) 67 | } 68 | } 69 | ``` 70 | 71 | 72 | 73 | 위 예제에서 메모리 관계를 보면, `var player2 = player1` 시점에서 `data.icon`의 **reference count가 2**인 것을 볼 수 있다. 74 | 75 | 이는 참조만 복사하고 같은 인스턴스를 가리키는 얕은 복사가 발생했음을 알 수 있다. 76 | 77 | 78 | 79 | 그러나 `PlayerClass` 객체를 복사하는 일이 발생할 때, 아래와 같이 `from` 매개변수가 있는 생성자를 사용하면 깊은 복사가 발생한다. 80 | 81 | ```swift 82 | func test() { 83 | let player1 = PlayerClass("🐸") 84 | var player2 = player1 85 | player2 = PlayerClass(from: player2) 86 | player2.data.icon = "🚚" 87 | assert(player1.data.icon == "🐸") 88 | } 89 | ``` 90 | 91 | 실은 이 개념이 CoW(Copy on Write)의 핵심적인 개념이다. 그리고 위 과정을 통해 참조 타입의 복사도 값 타입과 동일하게 동작할 수 있게 된다. 92 | 93 | 100 | 101 | # Copyable 102 | 103 | ## 등장 배경 104 | 105 | 우리는 구조체, 열거형, 클래스 등의 형태로 사용자 정의 자료형을 나타낼 수 있다. 106 | 107 | 구조체와 열거형은 값타입을 나타낼 수 있는 자료형이고, 클래스는 참조타입을 나타낼 수 있는 자료형이다. 108 | 109 | 하나의 고유한 객체를 표현하고 싶으면 클래스를 사용하는 것이 일반적인 방법이다. 110 | 111 | 그러나, 클래스의 경우 다음의 단점이 존재한다. 112 | 113 | - 힙 영역 메모리 관리 부담 (무한히 존재 가능) 114 | - 참조 카운팅(Reference Counting)으로 인한 오버헤드 발생 115 | 116 | 경우에 따라서는 별로 부담이 안되는 비용일 수 있다. 117 | 118 | 그러나, 시스템적인 한계가 존재하거나 자주 사용되는 로직 등에서는 부담이 되는 것 또한 사실이다. 119 | 120 | 이런 점에서 착안하여 값타입에 대해 고유한 소유권(unique ownership)을 부여하는 방식이 요구되었다. 121 | 122 | 즉, 고유한 값타입을 만들어서 참조 타입의 특성 중 하나인 고유성을 대체하고자 하는 것이다. 123 | 124 | ## Copyable이란 125 | 126 | 값타입이 고유할 수 없는 이유가 무엇일까? 127 | 128 | 그것은 **값을 복사할 수 있기 때문**이다. 이는 여러곳에서 동시에 같은 인스턴스가 존재할 수 있는 가능성이 있다. 129 | 130 | 그렇다면 값을 복사할 수 없게 하면 어떨까? 131 | 값을 복사할 수 없게(`NonCopyable`) 할 수만 있다면, 값타입이라도 유일하게 존재할 수 있을 것같다. 132 | 133 | 여기서 등장한 개념이 `Copyable`이다. 134 | 135 | `Copyable`은 (말 그대로) `복사가 될 수 있음`을 나타내는 프로토콜이다. 136 | 137 | 새로운 타입을 만들 때, 우리는 깊은 복사 가능 여부를 제어할 수 있다. 하지만 Swift가 자동으로 제공하는 복사에 대해서는 제어할 수 없다. 138 | 139 | 왜냐하면 일반적으로 이것을 채택한다고 명시할 필요 없이 자동으로 `Copyable`이 채택되기 때문이다. 140 | 141 | 암시적으로 `Copyable`을 준수하는 경우는 아래와 같다. 142 | 143 | - 구조체(Struct) 선언, 복사할 수 없는(non-copyable) 저장 프로퍼티가 없는 경우 제외 144 | - 열거형(Enum) 선언, 연관값이 복사할 수 없는 경우 제외 145 | - 클래스(Class) 선언 146 | - 액터(Actor) 선언 147 | - 프로토콜(Protocol) 선언 148 | - 연관타입(Associated Type) 선언 149 | - 프로토콜 확장(Protocol-Extension)의 Self 타입 150 | - 확장(Extension)에서는 확장되는 타입의 제네릭 매개변수 151 | 152 | 참조 타입인 클래스와 액터는 복사할 수 없는 저장 프로퍼티가 있어도 상관없다. 153 | (스스로의 참조값은 복사 가능하므로) 154 | 155 | # Non-Copyable (aka. Move-Only Type) 156 | 157 | 그런데 우리는 복사 가능한 값을 쓰고 싶은 것이 아니라 복사 불가능한 값이 궁금하다. 158 | 159 | 복사 불가능하다는 의미는 문자 그대로다. 160 | 161 | 이것은 `Copyable` 프로토콜을 준수하지 않도록 하면 달성할 수 있다. 162 | 163 | 간단히 `~Copyable`로 표현할 수 있다. 164 | 이를 `Copyable`을 자동으로 준수하지 않도록 `억누른다(suppress)`고 한다. 165 | 166 | ```swift 167 | struct A: ~Copyable { 168 | //... 169 | } 170 | ``` 171 | 172 | 이렇게 하면 `Copyable` 채택을 억제할 수 있다. 173 | 174 | - 이것은 어떤 특별한 프로토콜 제약사항을 추가하는 것이 아니다. 175 | 176 | 즉, 복사를 할 수 없게 한다. 177 | 178 | ## Non-Copyable 사용하기 179 | 180 | 고유한 값이 생겼지만, 복사를 할 수 없다는 단점이 생겼다. 181 | 182 | 프로그램에서 복사는 값을 전달하는 거의 유일한 방법이다. 183 | 184 | 함수에서의 사용, 대입 연산, 메소드 호출 등 생각보다 여러곳에서 값복사가 발생한다. 185 | 186 | 어떻게 사용해야할까? 187 | 188 | ### 소유권 (Ownership) 189 | 190 | 이에 대해 알아보기 위해서는 소유권이라는 개념에 대해 알아야 한다. 191 | 192 | Swift에는 소유권(Ownership)이라는 기본 개념이 있다. 193 | 194 | **값의 소유권(Ownership of Value)** 이란 **값의 표현을 관리할 수 있는 책임**을 말한다. 195 | 196 | 값 혹은 property를 사용할 때에는 언제나 이 소유권 시스템과 상호작용하며 Swift의 메모리 안전의 핵심이 된다. 197 | 198 | 값은 아래 3가지로 사용될 수 있다. 199 | 200 | - Consume it (소비될 수 있음) 201 | : 소유권을 완전히 옮기는 것을 의미 202 | - Mutate it (변경될 수 있음) 203 | : 임시 쓰기 접근 권한을 제공을 의미 204 | - Borrow it (빌려질 수 있음) 205 | : 소유권을 빌려주는 것을 의미 (읽기 전용) 206 | 207 | ### consume, borrow, inout 208 | 209 | 새로운 연산자 comsume(소비), borrow(차용) 두개가 생겼다. 210 | 211 | mutating(또는 inout)은 소비와 차용 둘 다 가능하지만 소비를 하면 새로운 값의 할당이 필요하다. 212 | (기본적으로 소유권 반환) 213 | 214 | 이전 연산을 통해 이후 가능한 연산을 정리하면 아래 표와 같다. 215 | 216 | | 전→후 (행→열) | consume | borrow | 217 | | --- | --- | --- | 218 | | consume | ✅ | ✅ | 219 | | borrow | ❌ | ✅ | 220 | | inout (mutating) | ✅ (반환시 돌려줘야 함) | ✅ | 221 | 222 | 예시를 통해 자세히 알아보자. 223 | 224 | ### 1. consume 225 | 226 | 227 | 228 | 위의 코드가 있다고 하자. 229 | 230 | 매개변수 자료형 앞에 `consuming`이라는 예약어를 붙이면 **해당 파라미터를 호출자로부터 가져온다**는 의미가 된다. 231 | 232 | 그러나 `newDisk` 메소드에서는 문제가 발생한다. 233 | 234 | 235 | 236 | newDisk 메소드에 있는 디스크 내용을 format 메소드 파라미터 disk에 넘겨주었기 때문이다. 237 | 238 | 이를 통해 한번 “소비”한 result라는 변수는 다시 사용될 수 없다. 239 | 240 | 사용자는 이를 컴파일 시간에 알게 되고 오류를 확인하여 처리해야 한다. 241 | 242 | ### 2. borrow 243 | 244 | 245 | 246 | borrowing으로 등록된 변수는 **읽기 권한이 부여**된다. (= let 바인딩처럼) 247 | 248 | 실제로 대부분의 모든 매개변수와 메소드는 이처럼 동작한다. 249 | 250 | 251 | 252 | 그러나 차이점으로는 명시적 차용된 인수는 소비나 변경이 불가능하다는 것이다. 253 | 254 | 위의 코드를 통해 보면, 잠시 빌려온 disk를 새 변수에 소비(소유권 이전)할 수 없다는 의미가 된다. 255 | 256 | ### 3. inout (mutating) 257 | 258 | 259 | 260 | format 메소드 내에서 쓰기 접근 권한이 있기에 매개변수를 소비할 수 있다. 261 | 262 | 단, **소비를 했다면** 함수가 종료되기 전 어느 시점에 inout 매개변수를 다시 초기화해야 한다. 263 | 264 | 265 | 266 | 왜냐하면, 매개변수로 들어온 disk에 대한 소유권을 반환해야 하는데, 소비가 되어서 없어졌기 때문이다. 267 | 268 | disk변수에 값을 재할당함으로써 함수가 끝났을 때 (그 값이 이전값과 동일하지 않아도 됨) 돌려주어야 한다. 269 | 270 | - 이는 struct의 mutating함수에서도 동일하게 작동한다. 271 | 272 | ```swift 273 | struct copyinging: ~Copyable { 274 | var num: Int 275 | 276 | mutating func change() { 277 | _ = self 278 | // ❌ Missing reinitialization of inout parameter 'self' after consume 279 | } 280 | } 281 | ``` 282 | 283 | 284 | ### NonCopyable의 deinit 285 | 286 | NonCopyable은 deinit을 구현할 수 있다. 287 | 288 | 왜냐하면, 고유의 값을 가질 수 있기 때문이다. 289 | 290 | 고유의 값을 가진다는 것은, 그 객체가 소멸하는 것을 추적할 수 있다는 것을 의미한다. 291 | 292 | 따라서 소멸 시기에 필요한 함수를 호출할 수 있다. 293 | 294 | 295 | 296 | - `discard` 연산을 사용하면 deinit을 호출하지 않고 인스턴스를 없앤다. 297 | 298 | # 프로토콜과 제네릭 활용 299 | 300 | 301 | 302 | 대부분의 모든 타입은 기본적으로 Copyable을 따른다. 303 | 304 | 위의 예제에서, Command 라는 구조체는 Copyable을 따르고, Runnable이라는 프로토콜 또한 Copyable을 따른다. 305 | 306 | 다만, 자동으로 적용되기에 따로 프로토콜 준수를 명시하지 않아도 된다. 307 | 308 | 이제 아래 코드를 살펴보자. 309 | 310 | ```swift 311 | struct BankTransfer: ~Copyable { 312 | consuming func run() { 313 | // ... 314 | } 315 | } 316 | ``` 317 | 318 | 위의 코드에서 `BankTransfer`는 **`Copyable`을 채택하지 않는다**. 319 | 320 | 💡 321 | `~Copyable`은 **NonCopyable을 채택한다는 의미가 아니다!** 322 | 323 | Swift에서 `Copyable`을 자동으로 **채택하지 않도록 억누른다**는 것이다. 324 | 325 | 326 | 327 | 328 | 이상황을 위의 다이어그램으로 나타낼 수 있다. 329 | 330 | ~Copyable이 Copyable을 포함하는 것이 어색하다는 생각이 들 수도 있다. 331 | 332 | 그러나 문맥적으로는 ~Copyable은 단순히 Copyable을 채택하지 않은 것들이다. 333 | 334 | 따라서, Copyable을 준수한다는 것은 단순히 ~Copyable 상태에서 Copyable에 해당하는 요구사항을 더 충족시킨 것에 불과하다. 335 | 336 | 그러므로, ~Copyable은 더 넓은 범위를 포함하고 Copyable의 제약사항을 구현한 타입들은 Copyable이라는 상대적으로 좁은 범위에 포함된다. 337 | 338 | 말로만 하면 와닿지 않을 수 있다. 그림으로 더 살펴보자. 339 | 340 | 341 | 342 | `Runnable`이라는 프로토콜을 만들었다고 하자. 343 | 344 | 기본적으로 `Copyable`을 준수하도록 설정된다. 345 | 346 | 347 | 348 | 만약, `~Copyable`이라고 하면 위와같이 표현할 수 있다. 349 | 350 | 왜냐하면, 말 뜻 자체가 `Copyable`제약이 없다라는 뜻이 되기 때문이다. 351 | 352 | 그러므로 `Copyable` 제약이 없는 `~Copyable` 또한 채택 가능하고 그보다 좁은 `Copyable` 제약을 구현한 타입들도 `Runnable`을채택할 수 있다. 353 | 354 | 355 | 356 | 위의 상태에서 `BankTransfer`가 `Runnable`을 채택하면, `Runnable` 제약을 따르면서 `Copyable`은 따르지 않으므로, 위 그림처럼 표현된다. 357 | 358 | 359 | 360 | 여기서 `execute`를 살펴보자, 타입 T의 제약조건을 `Runnable`하되, `Copyable`을 **채택하지 않아도 된다**고 했으므로 보라색으로 포함된 원에 해당하는 모든 타입이 활용할 수 있다. 361 | 362 | 반면, 이전 그림의 `execute`함수에서 T는 자동으로 `Copyable`제약을 따르므로 `Runnable`과 `Copyable`의 교집합에 해당하는 타입만 실행할 수 있다. 363 | 364 | ## 조건부 Copyable 365 | 366 | NonCopyable로 구현된 다음과 같은 타입이 있다고 하자. 367 | 368 | ```swift 369 | struct Job: ~Copyable { 370 | var action: Action? 371 | } 372 | ``` 373 | 374 | ```swift 375 | func runEndlessly(_ job: consuming Job) { 376 | while true { 377 | let current = copy job // Job이 NonCopyable 취급을 받기에 카피 불가능 378 | current.action?.run() 379 | } 380 | } 381 | ``` 382 | 383 | 위의 함수를 실행하려고 하면 Job은 Copyable을 채택하지 않았으므로 copy라는 연산을 할 수 없다. 384 | 385 | 그런데 copy가 필요한 작업이라면 어떻게 해야할까? 386 | 387 | ```swift 388 | extension Job: Copyable where Action: Copyable { } 389 | ``` 390 | 391 | 구조체가 `Copyable`을 준수하려면 기본적으로 하위 속성도 `Copyable`을 준수해야 한다. 392 | 393 | 따라서 조건부로 하위 타입이 `Copyable`을 준수할 때 자신도 `Copyable`을 준수하도록 작성할 수 있다. 394 | 395 | 396 | 397 | 이를 위와 같이 다이어그램으로 표현할 수 있다. 398 | 399 | # 동시성과 관계 400 | 401 | Sendable은 동시성환경에서 서로다른 도메인으로 보낼 수 있음을 나타낸다. 402 | 403 | 이는 주로 데이터를 복사해서 해당 도메인에 격리함으로써(캡쳐) 달성한다. 404 | 405 | 그런데 NonCopyable의 경우 복사를 할 수 없는데 동시성 환경에서 어떻게 동작할까? 406 | 407 | Sendable타입은 기본적으로 Copyable이다. (Copyable이라고 Sendable은 아님) 408 | 409 | 그러나 NonCopyable도 동시성에서 사용할 수 있도록 **예외적으로 Sendable을 허용**한다. 410 | 411 | - 이때 NonCopyable 타입은 Sendable을 명시적으로 채택해야 한다. 412 | 413 | ```swift 414 | struct Copying2: NCop, ~Copyable, Sendable { 415 | var num: Int 416 | } 417 | ``` 418 | 419 | 비동기 함수에서 사용할 떄에도 소유권에 대해 조절하면서 사용해야 한다. 420 | 421 | 만약 Task를 쓰면 캡쳐가 안된다. 422 | 423 | ```swift 424 | let globalA = copyinging(num: 1) 425 | func good() { 426 | let a = copyinging(num: 1) 427 | let b = a 428 | let c = globalA 429 | // ❌ Cannot consume noncopyable stored property 'globalA' that is global 430 | // let d = a 431 | var e = b 432 | e.num = 2 433 | e.num = 3 434 | 435 | Task { 436 | let f = e 437 | // ❌ Noncopyable 'e' cannot be consumed when captured by an escaping closure 438 | 439 | } 440 | } 441 | ``` 442 | 443 | --- 444 | 445 | ### 참고자료 446 | 447 | https://green1229.tistory.com/526 448 | 449 | https://developer.apple.com/videos/play/wwdc2024/10170 450 | 451 | https://developer.apple.com/documentation/Swift/Copyable 452 | 453 | https://developer.apple.com/videos/play/wwdc2024/10217 454 | 455 | https://github.com/swiftlang/swift-evolution/blob/main/proposals/0432-noncopyable-switch.md 456 | 457 | https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md 458 | 459 | https://github.com/swiftlang/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md -------------------------------------------------------------------------------- /Week6/Images/38a38e1c-e402-4403-97e3-4eb3dbbb61a3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/38a38e1c-e402-4403-97e3-4eb3dbbb61a3.png -------------------------------------------------------------------------------- /Week6/Images/image 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 1.png -------------------------------------------------------------------------------- /Week6/Images/image 10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 10.png -------------------------------------------------------------------------------- /Week6/Images/image 11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 11.png -------------------------------------------------------------------------------- /Week6/Images/image 12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 12.png -------------------------------------------------------------------------------- /Week6/Images/image 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 2.png -------------------------------------------------------------------------------- /Week6/Images/image 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 3.png -------------------------------------------------------------------------------- /Week6/Images/image 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 4.png -------------------------------------------------------------------------------- /Week6/Images/image 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 5.png -------------------------------------------------------------------------------- /Week6/Images/image 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 6.png -------------------------------------------------------------------------------- /Week6/Images/image 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 7.png -------------------------------------------------------------------------------- /Week6/Images/image 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 8.png -------------------------------------------------------------------------------- /Week6/Images/image 9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image 9.png -------------------------------------------------------------------------------- /Week6/Images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/image.png -------------------------------------------------------------------------------- /Week6/Images/스크린샷_2025-02-06_19.17.28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/스크린샷_2025-02-06_19.17.28.png -------------------------------------------------------------------------------- /Week6/Images/스크린샷_2025-02-06_19.19.10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/스크린샷_2025-02-06_19.19.10.png -------------------------------------------------------------------------------- /Week6/Images/스크린샷_2025-02-06_19.23.38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/스크린샷_2025-02-06_19.23.38.png -------------------------------------------------------------------------------- /Week6/Images/스크린샷_2025-02-06_19.25.47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Concurrency-Study/SwiftConcurrencyStudy/ff742a6b4c3bbc1632ce29752dbb60374cef891c/Week6/Images/스크린샷_2025-02-06_19.25.47.png --------------------------------------------------------------------------------