ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동시성 프로그래밍(3) DispatchQueue
    iOS 2022. 2. 10. 23:57

    안녕하세요.

    오늘은 DispatchQueue에 대해서 알아보도록 하겠습니다.

     

    https://developer.apple.com/documentation/dispatch/dispatchqueue

     

    Apple Developer Documentation

     

    developer.apple.com


    https://www.karlrupp.net/2018/02/42-years-of-microprocessor-trend-data/

     

    개발자 분들이라면 많이 보셨을 그래프라고 생각됩니다.

     

    2000년도 초반까지 CPU는 단일 코어로 이루어져 있었습니다.

     

    그러나 점점 하나의 코어의 성능을 올리는데에는 한계가 왔습니다.

     

    CPU의 발열을 잡을 수 없게 된 것이죠. 

     

    표를 보시면 2000년도 중반부터 단일 쓰레드의 성능과 주파수가 수렴하는 구간이 나타나는 것을 확인할 수 있습니다.

     

    따라서 CPU 개발사들은 하나의 코어 성능을 올리는 것을 멈추고 코어의 개수를 늘리는 방법을 선택했습니다.

     

    이로 인하여 개발자들이 해야 할 일이 증가했습니다.

     

    앱이 멀티 프로세스를 지원해야 하는 것이죠.

     

    NSThread를 통해 쓰레드를 만드는 방법 또한 여전히 유효합니다.

     

    그러나 개발자들에게 스레드를 만들라고 하니 병목현상이 생기거나 스위칭이 잘 되지 않아 오히려 퍼포먼스가 떨어지는 경우가 생겼습니다.

     

    이러한 연유로 나타난 것이 Grand Central Dispatch(GCD)입니다.


    GCD는 개발자에게 쓰레드를 만들도록 하지 않습니다.

     

    스레드는 운영체제가 직접 관리합니다.

     

    개발자가 해야 할 일은 작업을 클로저 단위로 나눠 큐(Queue)에 넣어주는 것이죠.

     

    그러면 큐는 큐의 속성에 따라서 작업을 스레드로 보냅니다(Dispatch).

     

    개발자들은 메인 쓰레드를 제외하고는 어떤 쓰레드에서 작업이 실행되는지 보장할 수 없습니다.


    DispatchQueue는 크게 3가지 종류로 볼 수 있습니다.

     

    1. Main

    2. Global

    3. Custom

     

    하나씩 살펴보도록 하겠습니다.


    1. Main Queue

    https://developer.apple.com/documentation/dispatch/dispatchqueue/1781006-main

     

    Apple Developer Documentation

     

    developer.apple.com

     

    공식 문서에서는 현재 프로세스의 메인 쓰레드와 관련된 Dispatch Queue라고 설명하고 있습니다.

     

    앱을 만들면 시스템이 자동으로 단 한 개의 main Queue를 만듭니다.

     

    main Queue로 보내진 작업은 오직 메인 스레드에서만 실행됩니다.

    따라서 Serial Queue입니다.

     

    기본적으로 아무런 처리를 하지 않고 코드를 작성하게 되면 작업은 메인 스레드에서 실행됩니다.

     

    그 말은 즉

    print("something")

    이렇게 작성한 코드가 사실은

    DispatchQueue.main.sync {
        print("something")
    }

    이러한 의미가 숨겨져있던 것입니다.

     

    대부분의 운영체제는 UI와 관련된 작업을 메인 스레드에서 합니다.

    iOS 역시 마찬가지입니다.

     

    UIKit의 공식문서에는

    특별한 명시가 없는한 UI 작업을 main Queue에서 실행해야 한다고 말하고 있습니다.


    2. Global Queue

    https://developer.apple.com/documentation/dispatch/dispatchqueue/2300077-global

     

    Apple Developer Documentation

     

    developer.apple.com

     

    특정 quality-of-service를 가지는 global Queue를 반환한다고 합니다.

     

    우선 quality-of-service를 알아봅시다.


    QoS(Quality-of Service)

    https://developer.apple.com/documentation/dispatch/dispatchqos

     

    Apple Developer Documentation

     

    developer.apple.com

    작업의 우선순위라고 보면 될 것 같습니다.

     

    앱이 가질 수 있는 자원이 한정적이기 때문에

    작업의 중요도에 따라 적합한 우선순위를 가지고 있는 큐에 넣어주어야

    반응성이 좋고 전력 낭비가 없는 앱을 만들 수 있습니다.

     

    QoS에는 총 6 종류가 있는데요.

    우선순위가 높은  순으로 알아보도록 하겠습니다.

     

    1. userInteractive

     

    가장 중요도가 높고 빠른 반응이 요구되는 작업일 경우 사용합니다.

    공식 문서에서는 애니메이션, 이벤트 처리 그리고 UI를 업데이트하는 경우 사용한다고 설명하고 있습니다.

     

    많은 자료에서 이 우선 순위를 가진 Queue에서 작업을 실행시키면 메인 스레드에서 동작한다고 설명하고 있습니다.

     

    아마 2015년 WWDC - Building Responsive and Efficient Apps with GCD 를 보고 그렇게 이해한 것 같습니다.

    The first is user interactive. This is the main thread. Imagine you have an iOS app, the user has their finger on the screen and is dragging. The main thread needs to be responsive in order to deliver the next frame of animation as the user is dragging. The main user interactive code is specifically the code needed in order to keep that 60 frames per second animation running smoothly.

    그래서 직접 확인해보았습니다.

    let queue = DispatchQueue.global(qos: .userInteractive)
    
    queue.async {
        print(Thread.isMainThread)
        print(Thread.current)
    }

    결과는 다음과 같습니다.

    userInteractive Global Queue는 메인 스레드에서 동작하는 것을 의미하지 않습니다.

    메인 쓰레드는 userInteractive의 우선순위를 갖지만 그 역은 성립하지 않습니다.

     

    2. userInitiated

     

    두 번째로 높은 우선순위입니다.

     

    사용자가 즉시 필요한 작업이지만, 비동기적으로 처리되는 작업에 적합합니다.

     

    이 작업이 이루어지지 않고서는 사용자가 의미 있는 상호작용을 진행하기 어려울 때 사용합니다.

     

    WWDC에서는 메일 앱에서 메일을 클릭했을 때 메일을 로드하는 작업에 사용한다고 설명하고 있습니다.

     

    3. default

     

    qos 옵션을 주지 않고 Global Queue를 생성했을 때 적용되는 기본값입니다.

     

    우선순위를 신경쓰지 않는 일반적인 작업에 사용합니다.

     

    4. utility

     

    사용자가 즉각적으로 결과를 얻을 필요가 없는 경우에 사용됩니다.

     

    주로 상태바(Progress Bar)와 함께 실행되는 길게 실행되는 다운로드 같은 경우에 적합합니다.

     

    5. background

     

    유저가 직접적으로 인지할 필요가 없는 작업에 사용됩니다.

     

    데이터를 미리 받아오거나, 데이터베이스 유지, 백업과 같은 서비스를 유지하거나 정리하는 작업에 적합합니다.

     

    6. unspecified

     

    qos 정보가 없을 때 사용합니다.

    시스템에 qos를 추론해야 한다고 알릴 때 사용합니다.


    이렇게 QoS에 대해서 알아보았습니다.

     

    시스템은 기본적으로 unspecified를 제외한 5 종류의 Global Queue를 가지고 있습니다.

     

    Global Queue는 Concurrent한 특성을 가지고 있습니다.

    따라서 작업을 여러 개의 스레드로 보내서 분산 처리합니다.

     

    QoS에 따라서 여러 개의 스레드를 작업에 배치하고 배터리를 더 집중적으로 사용하도록 합니다.

     

    또한 Queue가 아닌 작업에도 QoS를 설정할 수 있습니다.

    DispatchQueue.global(qos: .userInitiated).async {
        sleep(5)
        print("UserInitiated Queue")
    }
            
    DispatchQueue.global().async(qos: .userInteractive) { // 이런 식으로 async 뒤에 작업의 우선순위 지정 가능
        sleep(5)
        print("Default Queue")
    }

    위에 코드를 실행시키면 

    userInitiated Queue의 QoS가 더 높음에도 불구하고 Default Queue의 작업이 먼저 실행되는 것을 확인할 수 있습니다.

     

    Default Queue의 userInteractive의 QoS를 가진 작업이 들어갔기 때문입니다.

     

    Queue의 QoS보다 QoS가 높은 작업이 들어온 경우 Queue의 QoS가 일시적으로 작업의 QoS를 따라서 증가합니다.

     

    작업의 QoS가 낮은 경우에는 Queue의 QoS는 변하지 않습니다. 


    3. Custom Queue

    https://developer.apple.com/documentation/dispatch/dispatchqueue/2300059-init

     

    Apple Developer Documentation

     

    developer.apple.com

    시스템이 기본적으로 제공하는 Queue가 아닌 개발자가 직접 Custom Queue를 생성하는 것도 가능합니다.

     

    Custom Queue는 serial 하게도, concurrent 하게도 생성할 수 있습니다.

    let serialQueue = DispatchQueue.init(label: "serial") // default: serial
    let concurrentQueue = DispatchQueue.init(label: "concurrent", attributes: .concurrent)

    아무론 옵션을 주지 않으면 serial Queue가 생성되고, concurrent 옵션을 주게 되면 concurrent Queue를 생성할 수 있습니다.

     

     

    Custom Queue가 Global Queue에 비해서 가지는 장점을 알아보겠습니다.

     

    우선 식별자(label)를 가지고 있는 Custom Queue는 디버깅 시에 유리합니다.

     

    또한 barrier와 같은 세부적인 설정은 Custom Queue에서만 할 수 있습니다. 

     

    suspend()와 resume()도 직접 만든 큐에서는 동작합니다.

     

     

    Custom Queue도 Global Queue와 마찬가지로 QoS를 설정할 수 있습니다.

    하지만 QoS를 설정하지 않았을 시 놀랍게도 시스템이 알아서 QoS를 추론하는데요.

     

    그 이유는 Custom Queue QoS의 기본값이 Global Queue와는 다르게 unspecified이기 때문입니다.

    unspecified는 시스템에게 QoS를 추론해야 한다고 알립니다.

     

    또한 Custom Queue는 사실 Global Queue에서 실행됩니다.

    이는 코드를 통해 확인할 수 있습니다.

    let concurrentQueue = DispatchQueue.init(label: "concurrent", qos: .default, attributes: .concurrent)
    
    concurrentQueue.async {
        Dispatch.dispatchPrecondition(condition: .onQueue(DispatchQueue.global()))
        print("success")
    }

    dispatchPrecondition()은 조건이 맞지 않을 시 프로그램을 중지시킵니다.

     

    default QoS를 가진 Custom Queue를 만들고 작업을 해당 Queue에서 실행시켰을 경우

    Default Global Queue에서 실행되는 것을 확인할 수 있습니다.


    Queue들의 종류를 알아보았습니다.

     

     

    DispatchQueue를 사용할 때 주의해야 할 점이 있습니다.

     

    첫 번째는 Main Queue에서 다른 Queue로 작업을 보낼 때는 sync 메서드를 호출하면 안 된다는 점입니다.

    // 현재 메인 쓰레드
    DispatcgQueue.global().sync {
        // Task
    }

    이 코드를 해석하면 "메인 스레드에서 Global Queue에 보낸 작업이 완료될 때까지 대기한다."라는 의미라는 것을 이제 아실 겁니다.

     

    저희는 메인 스레드가 UI와 관련된 작업을 처리한다는 것을 압니다.

     

    따라서 메인 스레드에서 동기적으로 작업을 보내게 되면 유저한테 반응이 느리게 와 앱이 버벅거리게 됩니다.

     

    두 번째는 "현재의 Queue에서 현재의 Queue로 동기적으로 작업을 보내면 안 된다"입니다.

     

    동기적으로 작업을 보내면 작업을 보내는 스레드는 보낸 작업이 완료될 때까지 스레드를 Block 하게 됩니다.

     

    그런데 Block이 된 스레드에 다시 작업을 보내게 된다면??

     

    작업이 진행되지 않는 교착상태(Deadlcok)에 빠지는 것이죠.

     

    만약 현재 Queue가 Serial Queue라면 항상 교착상태에 빠질 것이고, Concurrent Queue일지라도 같은 쓰레드에 작업을 보낼 수 있기 때문에 발생 가능성을 내포하고 있습니다.

     

    DispatchQueue.main.sync를 하면 안 되는 점도 이와 같은 이유입니다.

    DispatchQueue.main.sync {
    	//do something
    }

    아무런 작업을 하지 않았을 경우 메인 스레드에서 동작하는데

    DispatchQueue.main.sync를 하게 되면

    Main Queue는 Serial Queue이기 때문에 교착상태에 빠지는 것입니다.

     

    만약 현재 Queue가 Main이 아니라면 DispatchQueue.main.sync를 호출해도 문제는 발생하지 않습니다.

    실제로 Background Thread에서 실행 중인 작업 중 UI를 표시하기 위해 선행되거나 후행되어야 할 작업이 있을 경우 main.sync를 하기도 합니다.

    예시: https://developer.apple.com/documentation/photokit/phphotolibrarychangeobserver

     

    Apple Developer Documentation

     

    developer.apple.com


    오늘은 DispatchQueue에 대해서 알아보았습니다.

     

    잘못된 정보나 더 알려주시고 싶은 내용 알려주시면 감사하겠습니다.

     

    Ref

     

    https://www.inflearn.com/course/iOS-Concurrency-GCD-Operation/dashboard

     

    iOS Concurrency(동시성) 프로그래밍, 동기 비동기 처리 그리고 GCD/Operation - 디스패치큐와 오퍼레이션

    동시성(Concurrency)프로그래밍 - iOS프로그래밍에서 필요한 동기, 비동기의 개념 및 그를 확장한 GCD 및 Operation에 관한 모든 내용을 다룹니다., - 강의 소개 | 인프런...

    www.inflearn.com

     

    https://demian-develop.tistory.com/7

     

    iOS) GCD에 대한 고찰 - 1(Dispatch Queue)

    iOS 개발을 하면서 멀티스레딩 작업이 필요할 때 사용하는 GCD에 대해 알아보려 합니다. 멀티스레딩 작업은 Operation으로도 할 수 있지만 이 글에선 GCD에 대해 다뤄보려 합니다. (Operation은 다른 글

    demian-develop.tistory.com

     

Designed by Tistory.