최근 회사에서 프로젝트로 공부할 시간이 전혀 없어서 주말을 이용해
개념을 정립하는 식의 공부를 하기로 했다. 절대적인 시간을 늘리기로
다짐 했기 때문에 앞으로는 퇴근하고도 블로그 작성을 멈추지 않기로했다.
멀티코어 하드웨어에서 시스템 적으로 관리되는 큐에게 일을 넘겨주는 것으로 코드를 동시적으로 실행시킨다.
디스패치 또는 Grand Central Dipatch (GCD)는 언어적 특징과 런타임 라이브러리, 그리고 macOS, iOS, WatchOS 그리고 tvOS 등에 있는 멀티코어 하드웨어에서 실행되는 동시성 코드를 서포트 하기위해 시스템이 체계를 갖추고 종합적으로 발전하도록 하는 시스템 강화등을 가지고 있다.
BSD subsystem, Core Foundation 그리고 CoCoa APIs 등은 계속 위와 같은 강점들을 시스템과 당신의 앱이 더 빨리 , 더 효율적으로 그리고 향상된 반응을 실행하도록 확장해 왔다.
컴퓨팅 코어 수가 서로 다른 여러 컴퓨터 또는 여러 애플리케이션이 이러한 코어를 두고 경쟁하는 환경에서 단일 애플리케이션이 여러 코어를 효과적으로 사용하는 것이 얼마나 어려운지 생각해 보자.
시스템 레벨에서 동작하는 GCD는 실행되고 있는 앱의 수요를 더 수용한다, 그들에게 가능한 시스템 자원을 균형있게 매칭해준다.
순차적으로 동시적으로 당신의 앱의 메인스레드 또는 백그라운드 스레드에서 일의 실행을 관리하는 객체.
DispatchQueue는 당신의 애플리케이션이 블록 형태로 일을 제출할 수 있는 FIFO( First In First Out ) 큐이다. Dispatch queue는 일들을 순차적으로 또는 동시적으로 처리한다.
dispatch queues에 제출된 일은 시스템에서 관리하는 스레드 풀에서 실행된다. 당신의 앱의 메인스레드를 대표하는 dispatch queue를 제외하고는 시스템은 어떤 스레드를 사용할지 보장할 수 없다.
당신은 동기적으로 또는 비동기 적으로 당신의 일을 스케줄링 할 수 있고, 당신이 실행할 일을 동기적으로 스케쥴링할 때는, 당신의 코드는 실행이 끝날 때 까지 기다린다. 당신이 실행할 코드를 비동기적으로 스케줄링 한다면, 당신의 코드는 당신이 실행한 코드를 기다리지 않고 계속 실행된다.
과도한 스레드 생성을 피하기
일의 동시적 실행을 디자인 할 때, 스레드를 차단하는 메소드를 생성하지 말라. 동시적으로 dipatch queue에 스케줄링 된 테스크가 스레드를 막을 때, 시스템은 다른 큐잉된 동시적인 테스크를 실행시키기 위해 추가적으로 스레드를 발생시킨다. 만약 너무 많은 테스크 블럭이 있으면, 당신의 앱의 스레드는 바닥이 날 수도 있다.
앱이 너무 많은 스레드를 사용하는 다른 경우는 private concurrent dispatch queues를 사용하는 경우이다. 왜냐하면 각 dipatch queue는 thread 자원을 소모한다, 추가적인 concurrent dispatch queue를 만드는 것은 thread 소비 문제를 악화시킨다. 직렬 작업의 경우, 당신의 시리얼 큐중 global concurrent queue로 쓸 대상을 설정한다. 이렇게 하면 당신은 큐의 직렬 처리를 유지하면서 동시에 분리된 여러 개의 큐가 스래드를 생성하는 것을 최소화 할 수 있다.
let serialQueue = DispatchQueue(label: "chacha.serial.queue")
serialQueue.async {
print("Task 1 started")
// Do some work..
print("Task 1 finished")
}
serialQueue.async {
print("Task 2 started")
// Do some work..
print("Task 2 finished")
}
/*
Serial Queue prints:
Task 1 started
Task 1 finished
Task 2 started
Task 2 finished
*/
let concurrentQueue = DispatchQueue(label: "chacha.concurrent.queue", attributes: .concurrent)
concurrentQueue.async {
print("Task 1 started")
// Do some work..
print("Task 1 finished")
}
concurrentQueue.async {
print("Task 2 started")
// Do some work..
print("Task 2 finished")
}
/*
Concurrent Queue prints:
Task 1 started
Task 2 started
Task 1 finished
Task 2 finished
*/
final class Messenger {
private var messages: [String] = []
private var queue = DispatchQueue(label: "messages.queue", attributes: .concurrent)
var lastMessage: String? {
return queue.sync {
messages.last
}
}
func postMessage(_ newMessage: String) {
queue.sync(flags: .barrier) {
messages.append(newMessage)
}
}
}
let messenger = Messenger()
// Executed on Thread #1
messenger.postMessage("Hello ChaCha!")
// Executed on Thread #2
print(messenger.lastMessage) // Prints: Hello Chacha!
상기의 코드는 데이터가 읽어짐과 동시에 입력하도록 세팅한 후 베리어를 쓰는 예제 코드이다.
flags: .barrier는 평행적으로 동시에 들어온 순간 병렬 큐를 직렬 큐로 바꾼다. 베리어와 함께 선언된 일은 앞서 제출된 일들이 모두 완료될 때 까지 연기된다. 마지막 작업이 끝난 이후 베리어 블록을 실행시키고 난 후 원래의 실행 상태로 돌아간다.
main dispatch queue는 애플리케이션의 메인스레드에서 작업을 실행시키는 전반적으로 이용 가능한 직렬큐이다. 메인 스레드에서는 앱의 UI를 업데이트 하는 곳으로 사용되기 때문에 main thread queue를 사용할 때는 주의해야한다. 그러므로, 앞서 설명한 dispatch API들을 다른 스레드에서 작업을 수행시키는 것은 가치가 있다. 우리는 무거운 일을 백그라운드 큐로 시작할 수 있고 이 일이 끝나면 메인큐로 돌아올 수 있다.
아마 많은 큐를 만들어서 앱의 퍼포먼스를 향상시키려고 할 것이다. 안타깝게도,
스레드를 생성하는 것은 리소스를 사용하기 때문에 과도한 스레드를 만드는 것은 피해야한다.
이러한 과도한 스레드 생성에 일반적인 두개의 시나리오가 있다.
너무 많은 blocking 되는 작업( await 를 사용하는 )을 병렬큐에 추가하면
앱에서는 다른 작업을 처리하기 위해서 추가적인 스레드를 만든다. 그리고 dipatch queues가 너무 많이 존재해도 스레드 자원을 모두 소모한다.
가장 좋은 방법은 글로벌 병렬 dispatch queues를 사용하는 것이다.
이렇게 하면 너무 많은 concurrnet queue를 생성하는 것을 막을 수 있을 것이다. 이것과는 별개로, 우리는 long-blocking task를 실행하는 것 자체를 주의해야한다.
DispatchQueue.global().async {
/// Concurrently execute a task using the global concurrent queue. Also known as the background queue.
}
글로벌 큐는 상기와 같이 만들 수 있다.
이 글로벌 병렬큐는 백그라운드 큐라고 알려져있기도 하다, 그리고 메인큐의 옆에서 사용된다.