ViewController에 다 때려넣어서 1000줄 넘는 코드도 건드려야 하고, 필터도 새로 만들어서 추가하긴 해야하는데 수정할 부분 리스트업을 해봐야겠다.
비동기와 동시성 처리에 관한 정리를 위해 찾아봤는데 생각보다 WWDC 영상이 많다.
(2014년 이전은 찾기 너무 어렵다...)
일단 WWDC 보면서 흥미롭거나 새로운 부분은 좀 정리를 해볼 필요가 있을 것 같다.
(비동기, 동시성 처리는 다른 글로 써보기로)
어차피 async, await 때문에 다시 한번 정리하긴 해야 한다.
(WWDC와 OS 개념 비교하며 계속해서 찾아볼 예정)
Apple이 iOS 내부 코드를 오픈하지 않는 한 정확한 원리는 알 수가 없으니
WWDC와 문서로 밝힌 정보 파편들 + "이렇게 작동하니까 이럴 것이다"라는 리버스 엔지니어링을 기반으로 수 많은 사람들이 학습하고 설명하고 있다.
레지스터, 캐시, 메모리 스왑 등 추가 개념은 다 빼고 단순 CPU와 메모리만 집중해서 간단한 흐름만 살펴보자.
과거 프로그램은 범용성보다 목적성이 우선이었기에 복잡하지 않았고, 메모리로 올라온 프로세스가 작업의 단위였다. 이에 CPU도 싱글 코어 위주였으며 프로세스를 처리하면 되었다.
이에 CPU 개발 회사들은 CPU 코어의 퍼포먼스를 높이는데 집중해서 2000년대 4.0GHz에 도달하는 clock speed를 얻을 수 있었다.
싱글 코어 성능을 지속적으로 향상하면 동시성 처리가 필요없지 않을까?
싱글 코어 성능이 무작정 오르면 (=clock speed가 높아지면)
모바일 기기에서는 효율적인 배터리 사용이 중요해지는데, 이에 반대되는 상황이 펼쳐질 수 있다. 또한 더 많은 전력 소비는 필연적인 발열 해소 문제가 대두된다.
![]() | ![]() |
---|---|
![]() | ![]() |
24코어 CPU라서 1:1 대응은 어렵지만 단순 비교로 intel의 i9-13900K의 성능은 부스트클럭으로 최대 5.8GHz의 퍼포먼스를 낼 수 있지만 순간 250W 수준의 전력 소비를 한다.
250W의 전력 소비가 와닿지 않을 수 있는데, 전성비가 달라서 정확한 비교는 어렵지만 2019년 정부 발표로 벽걸이 에어컨이 시간당 500W 전력 소비를 한다.
이정도의 전력 소비로는 모바일 기기 일일 활용이 불가능하다.
(참고: A16 Bionic 스펙: 최대 8W 소비 전력)
메모리와 저장소의 성능이 올라오지 못하면 속도 차이로 인한 병목현상 발생
성능 향상을 위한 input resource 비용은 많이 들지만 그 만큼의 성능 향상 output이 적게 나타나게 된다. (한계 효용의 법칙)
이로 인한 가격 상승 부담이 불가피해진다. (+ 전력 소비, 발열)
![]() | ![]() |
---|
(i9-13900K의 가격, 8월 말 대비 가격이 상승한 메모리 가격)
이로 인해 CPU 회사들은 싱글 코어에서 코어 수를 늘려 성능 향상을 하는 방향으로 멀티 코어에 집중하기 시작했다.
멀티 코어 시스템
여러 코어에게 작업을 나눠서 시킨다. 퍼포먼스를 위해 최대한 많은 코어가 계속해서 일을 해야 한다. 이를 위해 노는, 대기중인, idle 상태의 코어가 존재하는 상황이 최소한으로 나타나도록 해야 한다.
한 CPU Cyle동안 많은 task를 얼마나 빠르게 끝낼 수 있을 지 관련해서 parallelism (병렬처리) 및 pipelining으로 퍼포먼스를 더 높이려 시도한다.
이미 1960년대에 싱글 코어 환경에서 프로세스의 동시성 처리에 관한 논문이 등장하긴 했다.
멀티코어 환경으로 넘어가는 와중에 컴퓨터가 점점 범용화되면서 프로그램들은 점차 규모가 커졌고 복잡해졌다. 싱글코어 하나로 모든 작업을 serial하게 처리하기에는 시간이 오래 걸렸다. 또한 프로세스 하나가 끝날 때까지 나머지 프로세스는 대기 중으로 전체 작업 진행이 느려졌다.
e.g.) 3분동안 이미지 로딩이 진행되는동안 배경화면의 시계가 작동하지 않는다고 상상해보자.
이에 멀티 프로세스(multi process)로 프로세스 여럿을 동시에 처리할 수 있게 되었다.
내부에 작업 timer가 있다고 가정해보자.
1) CPU가 해당 timer 끝날 때까지 프로세스 처리한다.
2) timer가 종료되면 처리하던 프로세스의 state를 PCB (Program Counter Buffer)에 저장
3) 대기중인 다름 프로세스를 가져오고 timer reset
4) 다음 프로세스 시작, timer start
5) 반복...
각 프로세스가 메모리에 생성될 때마다 자신만의 코드-데이터-힙-스택의 메모리 영역을 차지하고, 이는 context switching이 부담되는 문제가 나타났다.
context-switching이란, CPU가 process scheduling에 의해 한 process를 수행 중 다음 process를 수행하게 될 경우, 전환을 위해 현재 process를 저장하고 다음 process의 상태값을 받아오는 것을 의미한다.
(원래는 한 task를 처리하다 다음 task를 받아오는 것을 의미하지만 맥락상 process가 task의 단위이므로 process로 설명)
멀티 프로세스로 인해 여러 프로세스를 진행할 수 있게 되었지만 CPU는 한 번에 하나의 작업을 아주 빠른 속도로 처리한다. 만약 CPU가 한 프로세스가 완료될 때까지 100% 점유로 해당 작업만 진행하면 다른 process들은 전혀 진척이 없고 대기 상태가 길어지게 된다. 이에 process scheduling으로 일정 타이밍 동안 한번에 하나씩 빠르게 처리하다 시간이 다 되면 다음 process를 처리하는 등 마치 멀티태스킹을 하는 듯 돌아가면서 처리한다.
multi-process 환경도 성능 향상을 이끌었지만 context-switch의 비용이 매우 비쌌다. 각 process마다 보유한 데이터와 코드가 다 다르므로 이전 process의 정보는 다음 process의 작업 진행에 도움이 되지 않는다. 따라서 매번 process를 "바꿀 때마다" 아무 작업도 할 수 없다.
이를 해결하기 위해 process보다 더 작은 작업 단위를 만들어 process의 교체 주기를 작게하려 시도하니 이가 Multi-Thread였다.
process 내 thread 수를 늘려 context-switching을 thread 기반이 되도록 한다. thread는 process 내부에서 stack을 제외한 code, data, heap을 공유하기에 context-switching에서 stack만 update하면 되는 장점이 있다. 이로 인해 외부적으로는 process 교체 빈도를 줄여 작업 효율을 높인다.
싱글 코어 환경에서는 thread간 교체로 마치 병렬로 처리되는 것 같은 효과를 주지만 실제로는 context-switching으로 빠르게 하나씩 처리한다. 멀티 코어 환경으로 오면서 각 코어마다 thread 처리가 가능하다.
e.g.) 4 cores, 2 threads: 8 threads at the same CPU cycle
![]() | ![]() |
---|
2011년 iPhone 4S부터 듀얼코어 A5가 탑재된 스마트폰이 되었다.
GCD는 2009년 Mac OS X 10.6 (Snow Leopard) 및 2010년 iOS 4부터 멀티코어 및 동시성 처리를 지원하기 위해 등장하였다.
모바일 컴퓨팅에도 멀티코어 시대가 오면서 Apple은 동시성 처리를 위해 GCD를 도입하고 관련된 세션을 여럿 발표한다.
과거처럼 C언어 스타일의 OS의 작업 scheduling을 고려한 프로그램을 작성해야 하지만 GCD를 도입한 Apple은 OS가 알아서 thread scheudling을 할 테니, 언제 필요한 thread를 활용할 지에 대한 API 활용을 개발자들이 잘 해줄 것을 요구한다.
Execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system.
comprehensive improvements to the support for concurrent code execution on multicore hardware in macOS, iOS, watchOS, and tvOS.
시스템 레벨에서 디바이스 스펙, OS 플랫폼 상관없이 개발자가 맡기기만 하면 알아서 처리해준다.
최적의 유저 경험은 유저 반응에 즉각적이어야 하기에 thread의 활용도를 나눈다.
OS가 많은 연산 처리와 I/O 처리를 효율적으로 하기 위해 작업 처리의 우선순위가 필요하다.
작업 처리의 우선순위를 시스템에게 직접적으로 전달, OS가 그에 맞게 알아서 처리하도록 의도 전달하는 작업이다. OS에 따라 구분할 필요 없이 개발자는 DispatchQueue로 task를 전달할 때 같이 입력만 하면 된다.
User-Interactive | User-Initiative | Default | Utility | Background |
---|---|---|---|---|
UI update, event handling 연관 처리 | UI 비동기 처리 | UI와 non-UI 사이 위치 | UI에 업데이트 될 데이터 작업 | 유저가 동작하는지 알지 못하는 작업들 |
유저 대응 | 유저 대응 지속적으로 해야 하는 경우 | global queue의 기본 설정 | I/O, network 비롯 오래걸리는 작업들 | 시작하자마자 background 순위로 작업해도 상관없는 작업들 |
main thread의 기본 설정 | 높은 우선순의 concurrent | 유저가 작업 진행 인지, 배터리 효율성 고려 많은 작업들의 처리가 이뤄줘야 함 |
높은 우선순위 | <---- | ----> | 낮은 우선순위 |
---|
높은 우선순위 작업이 낮은 우선순위 작업의 결과물을 resource로 필요한 경우
GCD가 Mac Pro부터 Apple Watch까지 여러 디바이스의 스펙과 플랫폼 환경에 따라 최적화를 "알아서" 해준다.
Apple 제품들이 모두 멀티코어 스펙이면서 iOS 9부터 iPad 멀티태스킹이 가능해지는 등 기능 확대가 되어가지만, Macbook air 및 아이폰을 비롯해서 Fanless 디바이스의 에너지 효율성을 고려하고 상황에 따른 싱글 코어 환경 가정에서 여러 thread가 실행되려면 QoS 순위로 처리해야 한다.
DispatchQueue로 넘어가는 작업들: 시스템이 자동으로 Initiative로 우선순위 내림
--> Automatic Propagation
Inferred QoS: DispatchQueue에 등록할 때의 QoS로, 시스템이 자동으로 변환한 QoS 포함
--> main으로 돌아와도 원래의 QoS로 돌아오지 않음
Network 통신과 같은 작업들: 오래 걸리지만 유저가 진행 상황을 알고 있는 작업들.
concurrent하게 처리 ~ 다른 thread 요청해서 동시에 일을 처리하도록 한다.
요청한 각 thread의 QoS도 Utility로 설정해서 작업을 수행하도록 한다.
메모리 clean-up과 같이 유저 활동과 상관없는 작업들은 Background Priority로 설정.
이 경우 task를 GCD Queue에 맡기면 automatic propagation으로 자동으로 task의 priority를 낮춘다.
logout 혹은 탈퇴 작업같이 task 중 유저가 진행 결과를 보기 원하는 경우, GCD Queue로 넘기는 task의 QoS를 낮출 수 없다. 이 경우 DispatchWorkItemFlags 중 enforceQoS로 구현 가능하다.
Prefer the quality-of-service class associated with the block.
This flag prioritizes the block's quality-of-service class over the one associated with the current execution context, as long as doing so does not lower the quality of service.
문제는 (Async) Priority Inversion이 발생한다. 이를 해결하기 위해 GCD는 DispatchQueue가 현재 작업하고 있는 task의 QoS를 높여서 해결하려 한다. (QoS override)
공유 자원을 serial하게 접근하는 경우, serial queue는 task가 완료할 때까지 해당 block을 lock하고 대기하는 queue들은 wait한다. 이 때는 해당 thread의 QoS로 설정한다.
문제는 더 낮은 QoS의 thread가 먼저 점유하는 경우, (Sync) Priority Inversion이 발생한다. GCD는 대기 중인 task의 QoS를 높여서 해결한다.
concurrent하게 작업: GCD Thread Pool에서 thread를 요청해서 task를 맡긴다.
DispatchQueue.async 중 필요한 resource가 접근 가능하지 않은 경우, thread는 해당 resource가 가능 할 때까지 대기한다. (waiting, blocking)
GCD는 코어 당 thread 1개 씩 수행하는 것이 목표이므로 waiting이 발생할 때마다 다른 thread를 새로 할당한다.
수 많은 block에서 waiting이 발생하면 결국 thread 생성이 계속해서 늘어나며 Thread Explosion이 발생한다.
Thread Pool에 등록된 thread 개수도 한계가 있으므로 등록된 모든 thread가 할당된 뒤 새로운 task가 등록될 경우에 deadlock이 발생할 수 있다.
해결책:
network나 I/O 비롯해서 최대한 async call을 활용하기
parallel하게 작업해야 하는 명확한 경우가 아니라면 왠만하면 serial queue로 시작하기
NSOperationQueue 활용하기
Block을 잘 나눠서 처리하기
Async와 Sync 섞어서 활용하는 상황을 피하기
많은 Block을 한번에 concurrent queue에게 등록하지 않기
DispatchSemaphore 활용
Semaphore, Mutex, Lock, Race Condition, Priority Inversion 등 OS 작업 과정을 좀 더 찾아보고 이해해야겠다.