- 앱은 여러개의 스레드를 가질 수 있다.
- 메인 스레드: 유저 인터페이스를 담당하는 스레드
- 메인 스레드에서 무겁거나 대기 시간이 긴 작업을 하면 그 시간동안은 유저 인터페이스가 블락된다.(바람개비가 돌거나 , UI가 멈추거나)
- 기타 스레드: 메인 스레드 이외의 스레드
- 스레드를 쓰면 여러개의 코드를 동시에 실행 시킬 수 있다.
- CPU는 코어는 한번에 하나의 스레드만 실행시킬 수 있다. -> 즉 병렬 실행은 물리적인 코어가 많아야 한다.
- 코드의 무결성을 유지하는 것은 병행 처리에서는 굉장히 어려운 작업이다. -> 경쟁상태에 진입하지 않도록 주의해야 한다.
- GCD - 스레드를 추상화 한 것
- DispatchQueue와 RunLoop
- DispatchQueue에 작업을 투입하면, 스레드를 생성하고 작업을 할당하며, 작업 이후 스레드 소멸까지를 책임지고 해준다.
- sync와 async
- async: 작업을 순서대로 빼서 스레드에 할당하고, 스레드가 끝나면 이를 다시 시스템에 돌려줌
- sync: 순서대로 빼는 것은 동일하나, sync일 경우는 queue의 컨트롤을 작업을 넣은 스레드로 넘기고, 그 작업이 끝나는 대로 다시 돌려 받음(즉 current Thread에서 실행됨). queue에 작업을 넣었던 스레드는 블록됨
- Thread를 직접 만든다면, RunLoop를 실행할 수도 있다.(선택)
- 메인 스레드는 RunLoop와 DispatchQueue를 반드시 모두 가지고 있는 특별한 스레드다.
- 메인 스레드에서 작업을 하지 않고 백그라운드에서 작업을 하게 만들고, 결과만 나중에 돌려 받는다.
let queue = DispatchQueue(label: "com.example.imagetransform")
queue.async {
let smallImage = image.resize(to:rect)
DispatchQueue.main.async {
imageView.image = smallImage
}
}
- 많다고 좋은 것은 아니다. 적절히 제한할 필요가 있다.
- 스레드 풀은 제한되어 있다.
- 블록되어 있는 스레드가 많으면 필요 이상의 스레드가 생성된다 -> 스레드 폭발
- 앱 구조와 디스패치큐
- 앱의 데이터 흐름을 받는 영역을 파악하라
- 그 영역들을 서브 시스템으로 나누고, 서브시스템 마다 큐를 하라씩 두라
- Chaining vs Grouping
- Chaining: 한 큐에서 작업을 끝내면 다른 큐로 넘기는 것
- Grouping: 여러 큐에서 작업을 이어 받는 것. 기다리는 작업을 그룹지어서 모든 작업이 끝나는 것은 노티받을 수 있다.
- 시리얼 큐를 이용한 상호 배제
- 다만 이 때 순환 참조로 인한 데드락을 주의해야 한다.
var count: Int {
queue.sync { self.connections.count }
}
- QoS
- 업무에 대한 명시적인 분류
- 개발자가 의도적으로 실행 우선 순위를 지정하는 것
- async로 작업을 넣을 때 작업별로 지정할 수도 있고, 큐 자체에 미리 지정해 놓을 수 도 있다.
- DispatchWorkItem
- 기본적으로 .async는 제출 시의 context(QoS, logging 등)를 캡쳐한다.
- 실행 옵션을 다양하게 조정하려면, DispatchWorkItem을 쓰면 된다.
- .assignCurrentContext를 쓰면 생성시의 context를 캡쳐한다.
- wait를 통해서 해당 workItem이 끝나는 것을 받을 수도 있다. -> 이 때 해당 workItem의 QoS는 wait하는 스레드의 QoS만큼 올라간다.
- 이는 DispatchWorkItem이 ownership 정보를 가지고 있기 때문이다.
- Semaphore와 Group은 이러한 ownership정보가 없다.
- Swift3와 Synchronization
- 전역변수는 원자적으로 초기화된다.
- 클래스 프로퍼티, 지연 프로퍼티는 원자적이지 않다.
- Lock
- Darwin에서 제공하는 C스타일의 Lock은 제대로 쓰기가 굉장히 어렵다.
- Foundation의 Lock: Objective-C 객체로 C스타일 구조체를 감싼 것
- 권장하는 것은 DispatchQueue.sync
dispatchPreconditon(.onQueue(expectedQueue)))
dispatchPrecondition(.notOnQueue(unexpectedQueue)))
- 병행 세계에서에 객체 생명주기
- 싱글스레드에서의 셋업
- Activate
- invalidation
- 싱글스레드에서의 해제
- 옵저버 패턴
protocol SubsystemObserving {
func systemStarted(...)
func systemDone(...)
}
class BusyController: SubsystemObserving {
var invalidated = true
init(...) { ... }
func activate() {
Datatransform.sharedInstance.register(observer: self, queue: DispatchQueue.main)
}
func invalidate() {
dispatchPrecondition(.onQueue(DispatchQueue.main))
invalidated = true
DataTransform.sharedInstance.unregister(observer: self)
}
deinit {
precondition(invalidated)
}
}
- deinit문제
- Observer를 등록으로 인한 다중 소유 문제
- 데드락 문제
- 그래서 invaildate를 명시적으로 호출하도록 해야 된다.
- 이때 invalidate 상태를 확인할 수 있도록 데이터를 남기는 것도 좋다.
- Dispatch 가 따르고 있는 옵저버 패턴
let q = DispatchQueue(label: "com.example.queue", attributes: [.autoreleaseWorkItem])
let source = DispatchSource.read(fileDescriptor: fd. queue: q)
source.setEventHandler { // 이벤트 처리로직 }
source.setCancelHandler { close(fd) }