├── 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 |
52 |
53 | 프로세스가 598개 존재함을 확인할 수 있는데, 프로세스들은 CPU 자원을 사이좋게 나눠 사용하고 있다.
54 |
55 |
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 | 
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 | 
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 | 
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 | 
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 | 
195 |
196 | multi processing: 여러개의 Core가 여러 작업을 병렬로 처리하는 것
197 |
198 | 
199 |
200 | multi tasking + multi processing
201 |
202 | 
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 | 
24 |
25 | 요런식으로 스레드가 많이 생성될 수 있고 관리가 필요함 (내 컴터에서는 6093개가 한계인듯)
26 |
27 | 이를 보완하고자 GCD가 등장했다.
28 |
29 | GCD는 thread pool 패턴에 기반한 작업의 병렬 처리를 구현한 것이다.
30 |
31 | ## DispatchQueue
32 |
33 | DispatchQueue 객체는 이를 실현하는 주된 방법이다.
34 |
35 | DispatchQueue 객체를 생성하고 작업을 할당하면 알아서 스레드를 만들고 작업을 수행한 후 스레드를 지운다.
36 |
37 | 
38 |
39 | Synchronous 하게 실행하는 경우 worker에서 동작하던 DispatchQueue가 해당 작업을 넣은 스레드로 이동해서 작업을 수행한다.
40 |
41 | 
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 |
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 | 
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 | 
255 | 
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 | 
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 | >
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 |
49 |
50 |
51 | 기존의 GCD는 비동기 작업이 끝났는 지의 여부를 Completion Closure를 통해 알려준다.
52 |
53 | 그러면 A 작업이 끝나면 B, B 작업이 끝나면 C, … 이를 비동기로 처리한다면 무수히 많은 Depth가 생겨 들여쓰기에 의해 가독성이 낮아질 것이다.
54 |
55 | 반면 Swift Concurrency는 아래와 같이 동작한다.
56 |
57 |
58 |
59 |
60 | 위 사진들의 코드는 동일한 로직인 것.
61 |
62 | await 키워드를 통해 실제 비동기 코드이지만, **동기처럼** 보이게 하는 효과를 지녀 가독성을 증가시킬 수 있다.
63 |
64 | ### 에러 핸들링 안정성
65 |
66 | URLSession을 통해서 이미지를 다운 받는 메소드가 있다고 하자
67 |
68 | 이미지 내려받는 걸 실패했을 때 예외처리하는 상황으로 둘을 비교해보겠다.
69 |
70 |
71 |
72 |
73 | GCD는 이미지를 성공적으로 내려받으면 컴플리션 핸들러의 첫 번째 파라미터로 이미지를 넘겨준다.
74 |
75 | 그러나 상태코드가 200이 아니거나, 내려받은 data가 Nil인 경우 nil을 줘야 한다.
76 |
77 | 개발자가 실패했을 때에 대한 에러처리를 잘 하면 문제가 없지만, 휴먼 에러등의 이유로 컴플리션 클로저 호출을 빼먹으면 문제가 될 수 있다.
78 |
79 | 매번 확인해야 하는 번거로움이 있음
80 |
81 | 추가로, Result를 쓰면 가독성은 더 심각해짐
82 |
83 |
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 |
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 |
153 |
154 |
155 | ## CompletionHandler → Async/await
156 |
157 | 서버에서 이미지 리스트를 불러오고 이미지에 대한 썸네일을 화면에 보여주는 과정이 있다고 해보자. 서버에서 가져온 정보를 `UIImage`로 변환하는 과정에는 아래와 같은 일련의 과정이 필요하다.
158 |
159 |
160 |
161 |
162 | 해당 과정을 살펴보면 하위 과정이 실행되기 위해서는 상위 과정에 대한 결과값이 필요하다. 즉, 위 과정들은 모두 차례대로 진행되어야함을 의미한다.
163 |
164 | `thumbnailURLRequest`나 데이터를 UIImage로 전환하는 `UIImage(data:)` 와 같은 메서드들은 결과가 매우 빠르게 도출되기 때문에 어떤 스레드에서 호출되어도 상관없으며 동기적으로 실행되어도 괜찮다.
165 |
166 | 하지만 `dataTask(with:completion:)` 이나 `prepareThumbnail(of:completionHandler:)` 와 같은 함수들은 실행하고 결과가 나오기까지 시간이 조금 걸린다. 따라서 SDK에선 비동기 함수를 제공하며 위와 같은 함수들은 비동기로 실행되어야한다.
167 |
168 | 그럼 위 과정을 이제 기존의 `completionHandler`를 통한 비동기 처리 방식으로 코드를 짜보자.
169 |
170 |
171 |
172 |
173 | 먼저 `thumbnailURLRequest(for:)` 메소드를 호출한다. 위에서 말했듯 이 함수는 동기적으로 호출되는 함수이기 때문에 빠르게 처리가 된다.
174 |
175 |
176 |
177 | 이후 `URLSessionDataTask` 를 동기적으로 만들고 비동기 작업을 시작하기 위해 따로 `task.resume()` 를 호출해야한다.
178 |
179 | 데이터를 다운로드 받는 것은 시간이 걸리는 작업이며, 그 동안 스레드가 block되지 않게 하기 위해서는 위와 같이 비동기 작업으로 처리해주는 것이 매!우! 중요하다.
180 |
181 |
182 |
183 |
184 | 다운로드 요청이 완료되면 completionHandler를 통해 data, response, error 값들이 옵셔널하게 도착한다.
185 |
186 | 만약 error가 발생했다면, completionHandler를 호출하여 에러 처리를 해줘야한다.
187 |
188 |
189 |
190 |
191 | 값이 잘 도착했다면, `UIImage(data:)` 를 호출하여 동기적으로 데이터를 `UIImage`로 변환시켜준다.
192 |
193 |
194 |
195 | 이미지가 잘 생성이 되었다면, 우리는 `prepareThumbnail` 메소드를 호출하고 또 completionHandler를 통해 값을 전달한다. 해당 과정이 이루어지는 동안 스레드는 unblocked되고 다른 작업을 할 수 있게 된다.
196 |
197 | 간단한 과정인데 일단 굉장히 장황하게 설명되었다.. 그럼 위 코드는 이제 완-벽 한걸까??
198 |
199 | 노노 .ᐟ.ᐟ
200 |
201 |
202 |
203 |
204 | 위 `guard-let` 구문을 보면 에러에 대한 처리 없이 그냥 함수를 종료시켜버린다 ! 따라서 UIImage를 생성하거나 썸네일을 생성하는데 실패했더라도, `fetchThumbnail` 의 호출부는 이를 알 수 없고, 이미지는 영영 업데이트 되지 않게된다..
205 |
206 |
207 |
208 |
209 | 이를 해결하기 위해선 모든 함수 return 경로에 error를 담은 completion을 호출해야한다.. 여기선 Swift의 기본 에러 핸들링 메커니즘을 사용할 수 없는 것이다. (error throw하는거 못 함;)
210 |
211 | 이렇게 completionHandler를 사용한 두 개의 동기, 두 개의 비동기 처리를 하는 함수를 완성시켰다.
212 |
213 | 근데 20줄 따리의 코드 중 무려 5줄의 미묘한 버그가 끼어있을 수 있는 에러를 담은 completionHandler가 끼어있다. ㅋㅋ 이게 맞냐고 ~
214 |
215 | 이걸 아 ~ 주 조금은 안전하게 만들 수 있다. 바로 Result 타입을 활용하는 방식이다.
216 |
217 |
218 |
219 | 음.. 근데 그냥 코드가 조금 더 길어지고 못생겨짐..
220 |
221 | 자 ~ 그럼 이제 이 못나고 불안전한 코드를 async/await을 활용하여 리팩토링해보자 !
222 |
223 |
224 |
225 | 먼저 함수를 작성할 때, `throws` 키워드 전에 `async` 키워드를 붙여준다. 에러를 던지지 않는 함수라면 그냥 화살표 전에 `async`를 붙여주면 된다.
226 |
227 | 그럼 이제 깔꼼하게 `fetchThumbnail` 함수는 UIImage를 반환하고, 에러가 발생하면 throw를 할 수 있게 되었다 !
228 |
229 |
230 |
231 | 맨 처음 `fetchThumbnail` 이 호출되면, 이 전과 같이 `thumbnailURLRequest` 를 호출한다. 이 함수는 동기함수로, 해당 작업을 하는 동안은 스레드가 block 된다.
232 |
233 |
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 |
242 |
243 | 이후 데이터를 `UIImage`로 변환시키고 thumbnail 프로퍼티에 접근하면 썸네일이 렌더링되기 시작한다. 썸네일이 생성되기 시작하면, 스레드는 또 다시 unblocked 되며 다른 작업을 할 수 있게 된다. 그리고 썸네일이 잘 생성되었다면 그걸 반환하고, 실패했다면 error를 throw하게 된다.
244 |
245 | 껄껄.. completionHandler로는 20줄이었던 코드가 단 6줄로 변-신 ~
246 |
247 | 심지어 depth가 깊어지지도 않은 완전 straight한 코드임..
248 |
249 | 위 코드에서 확인할 수 있듯, `async` 키워드는 함수에만 붙을 수 있는게 아니고 프로퍼티, 이니셜라이저 등에도 모두 붙일 수 있다.
250 |
251 | 저 `thumbnail`이라는 프로퍼티는 기본제공이 아니고 따로 만든 프로퍼티인데 그 코드를 살펴보자.
252 |
253 |
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 |
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 |
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 |
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 |
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 |
343 |
344 | 스레드가 함수 호출을 실행하면 새 프레임이 스택에 푸쉬,
345 |
346 | 해당 스택 프레임은 스택의 Top에 쌓이고 이에는 로컬 변수, 리턴 주소값 등이 포함되어 있다.
347 |
348 | 쌓인 스택 프레임은 함수가 끝나면 Pop 되어 사라진다.
349 |
350 | ## async 방식의 Stack Frame
351 |
352 |
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 |
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 |
405 |
406 | 따라서 3번이 끝나면, 위 사진처럼 save 스택 프레임이 add 스택 프레임을 대체하게 된다.
407 |
408 | ### save 메소드 동작 중 await
409 |
410 |
411 |
412 | save 메소드 내부에서 만약 await로 비동기 메소드를 호출해서 Suspend가 되었다고 가정하겠다.
413 |
414 | 그러면 해당 스레드는 스레드 점유권을 내주고 되고 해당 스레드는 다른 작업을 수행할 수 있게 된다.
415 |
416 | ### save 메소드 종료 후 Return 과정
417 |
418 | save 메소드가 동작할 차례가 되어 continuation에 의해 Heap에 있던 save 비동기 프레임이 스택에 쌓이게 되고,
419 |
420 | save 메소드 수행 후 작업을 마치면 [ID]를 반환한다.
421 |
422 |
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 |
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 |
471 |
472 | 두 가지 다른 URL로부터 데이터를 다운로드하는 예제가 있다.
473 |
474 | 현재 코드는 순차적 바인딩이다.
475 |
476 | 하나는 이미지를 받는 거고, 하나는 이미지에 대한 메타 데이터용.
477 |
478 | 이러면 imageReq를 통해 이미지를 받아올 때까지 기다리고,
479 |
480 | 그 후에 metadata를 받아올 때까지 기다려서 이미지를 만들고 반환을 하게 된다.
481 |
482 | 그리고 오류 가능성이 있기 때문에 `try await` 을 사용해서 호출해야 한다.
483 |
484 | async-let 도입
485 |
486 |
487 |
488 | 두 다운로드가 동시에 이루어질 수 있게 async-let을 사용하여 동시 바인딩을 한다.
489 |
490 | 이러면 Child Task에서 작업이 발생하기 때문에 try await을 사용하지 않아도 됨
491 |
492 |
493 |
494 | 이제 아래 블록들에서 data와 metadata 변수를 사용하기 전에 try await을 한다.
495 |
496 |
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 |
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 |
815 |
816 | 기존 GCD환경에서는 위에서처럼 여러 Thread가 CPU자원을 번갈아 가면서 사용되었다.
817 |
818 | 이럴 때 Thread가 많아지면 많아질수록 스케줄링이나 lock과 관련해서 대기 시간이 길어질 수 있었다.
819 |
820 |
821 |
822 | 하지만, SwiftConcurrency에서는 CPU당 Thread를 하나씩 할당한다.
823 |
824 | 그리고 Continuation이라는 객체를 통해서 각각의 실행 맥락을 보존한다.
825 |
826 | 이를 통해 우리가 지불해야하는 비용은 함수 실행 비용 밖에 없다.
827 |
828 | 실제 비동기 함수를 실행할 때를 살펴보자.
829 |
830 |
831 |
832 | 비동기 함수를 실행할 때 우리는 await을 붙이고 호출한다.
833 |
834 | 이때 시스템에게 제어권을 넘기게 되고 함수의 실행이 끝나면 원래 함수를 호출한 쪽으로 돌아와서 resume(계속진행)한다.
835 |
836 | 좀 더 자세히 보자.
837 |
838 |
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 |
860 |
861 | 하지만 SwiftConcurrency에서는 Heap에 보관된 Continuation에서 취사선택하면 되므로 우선순위가 높은 것을 먼저 실행 가능하다!
862 |
863 |
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
--------------------------------------------------------------------------------