Grand Central Dispatch (GCD)

최근 회사에서 프로젝트로 공부할 시간이 전혀 없어서 주말을 이용해
개념을 정립하는 식의 공부를 하기로 했다. 절대적인 시간을 늘리기로
다짐 했기 때문에 앞으로는 퇴근하고도 블로그 작성을 멈추지 않기로했다.

멀티코어 하드웨어에서 시스템 적으로 관리되는 큐에게 일을 넘겨주는 것으로 코드를 동시적으로 실행시킨다.

디스패치 또는 Grand Central Dipatch (GCD)는 언어적 특징과 런타임 라이브러리, 그리고 macOS, iOS, WatchOS 그리고 tvOS 등에 있는 멀티코어 하드웨어에서 실행되는 동시성 코드를 서포트 하기위해 시스템이 체계를 갖추고 종합적으로 발전하도록 하는 시스템 강화등을 가지고 있다.

BSD subsystem, Core Foundation 그리고 CoCoa APIs 등은 계속 위와 같은 강점들을 시스템과 당신의 앱이 더 빨리 , 더 효율적으로 그리고 향상된 반응을 실행하도록 확장해 왔다.

컴퓨팅 코어 수가 서로 다른 여러 컴퓨터 또는 여러 애플리케이션이 이러한 코어를 두고 경쟁하는 환경에서 단일 애플리케이션이 여러 코어를 효과적으로 사용하는 것이 얼마나 어려운지 생각해 보자.

시스템 레벨에서 동작하는 GCD는 실행되고 있는 앱의 수요를 더 수용한다, 그들에게 가능한 시스템 자원을 균형있게 매칭해준다.

  • BSD subsystem : BSD(Berkeley Software Distribution)는 1977년부터 1995년까지 미국 캘리포니아 대학교 버클리(University of California, Berkeley)의 CSRG(Computer Systems Research Group)에서 개발한 유닉스 운영 체제이다.

순차적으로 동시적으로 당신의 앱의 메인스레드 또는 백그라운드 스레드에서 일의 실행을 관리하는 객체.

DispatchQueue는 당신의 애플리케이션이 블록 형태로 일을 제출할 수 있는 FIFO( First In First Out ) 큐이다. Dispatch queue는 일들을 순차적으로 또는 동시적으로 처리한다.

dispatch queues에 제출된 일은 시스템에서 관리하는 스레드 풀에서 실행된다. 당신의 앱의 메인스레드를 대표하는 dispatch queue를 제외하고는 시스템은 어떤 스레드를 사용할지 보장할 수 없다.

당신은 동기적으로 또는 비동기 적으로 당신의 일을 스케줄링 할 수 있고, 당신이 실행할 일을 동기적으로 스케쥴링할 때는, 당신의 코드는 실행이 끝날 때 까지 기다린다. 당신이 실행할 코드를 비동기적으로 스케줄링 한다면, 당신의 코드는 당신이 실행한 코드를 기다리지 않고 계속 실행된다.

  • Import!
    동기적으로 실행될 일을 메인큐에 놓으려고 실행하면 Deadlock이 발생한다.

과도한 스레드 생성을 피하기
일의 동시적 실행을 디자인 할 때, 스레드를 차단하는 메소드를 생성하지 말라. 동시적으로 dipatch queue에 스케줄링 된 테스크가 스레드를 막을 때, 시스템은 다른 큐잉된 동시적인 테스크를 실행시키기 위해 추가적으로 스레드를 발생시킨다. 만약 너무 많은 테스크 블럭이 있으면, 당신의 앱의 스레드는 바닥이 날 수도 있다.

앱이 너무 많은 스레드를 사용하는 다른 경우는 private concurrent dispatch queues를 사용하는 경우이다. 왜냐하면 각 dipatch queue는 thread 자원을 소모한다, 추가적인 concurrent dispatch queue를 만드는 것은 thread 소비 문제를 악화시킨다. 직렬 작업의 경우, 당신의 시리얼 큐중 global concurrent queue로 쓸 대상을 설정한다. 이렇게 하면 당신은 큐의 직렬 처리를 유지하면서 동시에 분리된 여러 개의 큐가 스래드를 생성하는 것을 최소화 할 수 있다.

What is Serial 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
*/

What is Concurrent Queue ?

  • 병렬 큐
  • 동시에 여러개의 큐를 사용한다.
  • data race 가 일어날 확률이 존재한다.
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
*/

What is Data Race

  • 데이터레이스는 여러 개의 스레드가 하나의 메모리에 동기화 과정 없이 접근하고 최소 한 개의 데이터만 쓰여졌을 때 발생한다. 예를 들면, 우리가 메인스레드에서 한 배열에 있는 데이터를 읽는 도중에 백그라운드 스레드에서 현재 읽고 있는 배열에 새로운 값을 입력하고 있는 경우이다. Data Race는 이상한 테스트 결과와 알 수 없는 충돌의 원인이 된다. 그러므로 정기적으로 Thread Sanitizer 를 사용하는 연습을 하는 것이 좋다.

Using a barrier on a concurrent queue to synchronize writes

  • 병렬 큐에 동기적인 Write를 하는 베리어를 사용하라. Barrier Flag를 사용하면 더 안전한 스레드 운용을 할 수 있다.
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는 평행적으로 동시에 들어온 순간 병렬 큐를 직렬 큐로 바꾼다. 베리어와 함께 선언된 일은 앞서 제출된 일들이 모두 완료될 때 까지 연기된다. 마지막 작업이 끝난 이후 베리어 블록을 실행시키고 난 후 원래의 실행 상태로 돌아간다.

Asynchronous vs synchronous tasks

  • DispatchQueue 작업은 동기적으로 또는 비동기적으로 작동할 수 있다.
    주된 차이는 작업을 만들 때 발생한다. 동기적으로 시작하는 일은 작업이 끝날 때 까지 다른 스레드가 불리는 것을 막는다. 비동기적으로 시작한 일은 바로 리턴 되는데 다른 스레드들이 불리는 것을 막지 않는다. 만약 메인스레드에서 작업을 한다고 가정해보자, 오래 걸리는 동기 메소드를 사용하는 것을 스스로 막고싶을 것이다. 이것은 메인스레드를 막는 결과를 만들 것이며, UI는 아마 반응하지 않을 것이다.

How about the main thread?

main dispatch queue는 애플리케이션의 메인스레드에서 작업을 실행시키는 전반적으로 이용 가능한 직렬큐이다. 메인 스레드에서는 앱의 UI를 업데이트 하는 곳으로 사용되기 때문에 main thread queue를 사용할 때는 주의해야한다. 그러므로, 앞서 설명한 dispatch API들을 다른 스레드에서 작업을 수행시키는 것은 가치가 있다. 우리는 무거운 일을 백그라운드 큐로 시작할 수 있고 이 일이 끝나면 메인큐로 돌아올 수 있다.

Avoiding excessive thread creation

아마 많은 큐를 만들어서 앱의 퍼포먼스를 향상시키려고 할 것이다. 안타깝게도,
스레드를 생성하는 것은 리소스를 사용하기 때문에 과도한 스레드를 만드는 것은 피해야한다.

이러한 과도한 스레드 생성에 일반적인 두개의 시나리오가 있다.
너무 많은 blocking 되는 작업( await 를 사용하는 )을 병렬큐에 추가하면
앱에서는 다른 작업을 처리하기 위해서 추가적인 스레드를 만든다. 그리고 dipatch queues가 너무 많이 존재해도 스레드 자원을 모두 소모한다.

How to prevent excessive thread creation?

가장 좋은 방법은 글로벌 병렬 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.
}

글로벌 큐는 상기와 같이 만들 수 있다.
이 글로벌 병렬큐는 백그라운드 큐라고 알려져있기도 하다, 그리고 메인큐의 옆에서 사용된다.

Review

  • 쓸 줄은 알아도 정확히 뭔지 모른다면 안 쓰느니만 못하다는 생각이 들었다.
  • 개념을 정확히 하자는 스스로의 프로젝트를 계속하겠다.
  • 오늘 이 것을 공부하는 와중에도 회사에서 고쳐야겠다고 생각하는 코드들이 많다.
  • 베리어 부분은 실제로 적용하고 쓰는데는 학습시간이 좀 더 필요하다고 생각한다.
  • 현재도 깊게 공부한 것은 아니지만 당장 이번에 하고있는 iCloud Data Back up logic에 적용해서 해결할 수 있는 문제들이 많을 것 같다.

좋은 웹페이지 즐겨찾기