본문 바로가기

iOS/iOS

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

iOS 개발을 하면서 멀티스레딩 작업이 필요할 때 사용하는 GCD에 대해 알아보려 합니다. 멀티스레딩 작업은 Operation으로도 할 수 있지만 이 글에선 GCD에 대해 다뤄보려 합니다. (Operation은 다른 글에서 다뤄보겠습니다.), 직접적인 Thread 관리는 제대로 하기 굉장히 어렵기 때문에 대부분 API를 사용하게 됩니다.

GCD에 대해 조금 깊게 이해하기 위해서는 Apple의 운영체제에서 Thread Programming, Concurrency Programming에 대해서 알고 있으면 좋습니다. 가볍게 훑어봤는데, 전산학 관련한 내용이 많아, 나중에 Thread 관련 글에서 다뤄보려 합니다. 이 중 간단한 용어지만 헷갈릴 수 있어 정의를 다시 옮겨오겠습니다.(Reference)

Thread

Thread 용어는 코드에 대한 별도의 실행 경로를 뜻합니다. OS X, iOS는 POSIX threads API를 기반으로 구현되었습니다.

Process

Process 용어는 실행 중인 실행 파일(executable)을 뜻합니다. 여러 Thread를 가지고 있을 수 있습니다.

Task

Task 용어는 수행되어야 할 작업의 추상적인 개념을 뜻합니다.

 

 

 

"Concurrency by Tutorials" by Scott Grosch

 

GCD 관련 도서의 표지는 불가사리인데 여기에는 재밌는 의미가 있습니다. 아래는 인용구입니다.

These marine invertebrates, including the Royal Starfish (Astropecten articulatus) that is found on the cover of this book, operate in the spirit of concurrency, having adapted so that the parts of their bodies have multiple functions — feet that help it move, feel, see and breathe, for example — for a simple but optimal life.

불가사리는 Concurrency의 정신을 이어받아 몸의 각 부분이 여러 가지 기능을 합니다. 불가사리의 발은 움직이고, 느끼고, 보고 , 숨 쉬는 것에 도움을 줍니다. 이는 삶을 최적화한다고 하네요, 

 

Swift에서의 Concurrency 처리

현재(2021/02/04) 기준  Swift는 Closures를 이용해 다른 Thread에서 작업을 실행합니다. 하지만 곧 C#과 Typescipt처럼 async/await 패턴을 이용할 수 있게 될 예정입니다.

Grand Central Dispatch

GCD는 Swift에서 Multi-threading(Concurrency) 작업을 수행하는 API입니다.

좀 더 세부사항을 말하자면, GCD는 C언어로 구현된 라이브러리입니다. 목적은 리소스 가용성(availability of resources)에 따라 병렬(parallel)로 실행될 수 있는 Task(Method or Closure)를 Queue에 넣는 것입니다. Task는 시스템에서 관리되는 스레드 풀(Pool of Threads)에서 실행됩니다. GCD를 이용하면 개발자가 직접 스레드를 관리할 필요가 없다는 장점이 있습니다. 하지만 Task가 어떤 Thread에서 실행될지에 대한 보장은 없습니다. 예외로 Main Queue의 경우는 Main Thread에서 동작함이 보장됩니다. 

 

Synchronous and asynchronous tasks

Queue는 Synchronously or Asynchronously로 동작할 수 있습니다. 이는 간단히 말하자면 현재 run loop를 차단하는지 마는지에 대한 차이입니다. Concurrent Queue는 FIFO이지만, 이는 어디까지나 작업의 시작에 대한 FIFO이지, 어느 작업이 먼저 끝나는지는 보장하지 않습니다. 반면에 Serial Queue의 경우 Queue 내부에서 작업이 순차적으로 끝나게 구현되었기 때문에 완료 순서가 보장됩니다. (queue safety)

하지만 Serial Queue라고 해서 항상 동일한 스레드에서 실행됨이 보장되진 않습니다. (forums.swift.org/t/what-is-the-default-target-queue-for-a-serial-queue/18094/7)

Serial and concurrent queues

Serial과 Concurrent Queue의 차이는 간단히 말하자면 Queue내에 Tasks에 대한 Single Task 보장을 하는지 안 하는지에 대한 차이입니다.

주의해야 할 점은  Concurrent Queue라고 해서 반드시 둘 이상의 Task가 실행된다는 보장이 없습니다. 앱의 리소스가 부족할 경우 single Task로 실행될 수 있습니다.

Asynchronous doesn’t mean concurrent

Async와 Concurrent는 동시에 실행된다는 점에서 헷갈릴 수 있습니다. 하지만 이 둘은 별개의 개념입니다. 이에 대해선 좋은 글이 있어 이를 공유합니다. 이를 이해하면, Main Queue에서 Sync를 호출했을 때 Deadlock이 일어나는 이유를 알 수 있습니다.  

 

Dispatch queues

Queue를 만들게 되면, OS에서 하나, 혹은 여러 개 Thread를 만들어 Queue에 할당합니다. 이미 있는 Thread를 재사용할 수 도 있고 Thread를 생성할 수도 있습니다.

 

The main queue

App을 만들게 되면 Main Queue는 자동으로 생성됩니다. 이는 Serial Queue이며, UI작업을 담당하게 됩니다. 이는 Main Queue에 UI작업과 관련 없는 작업을 넣으면 성능 저하가 일어날 수 있음을 의미합니다.

 

Quality of service

Concurrent Dispatch Queue를 사용할 때는 Quality of service를 통해 Task의 우선순위를 지정할 수 있습니다. 우선순위가 높은 Task은 우선순위가 낮은 Task에 비해 시스템 리소스가 많이 소모됩니다. Concurrent Dispatch Queue를 사용하고 싶을 때 Custom Queue를 별도로 만들지 않고 우선순위에 따라 미리 만들어져 있는 GlobalQueue를  사용할 수 있습니다.

 

https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html
https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html
man dispatch_queue_create

 

Qos관련 글은 쉽게 찾을 수 있기 때문에 일일이 언급하지는 않겠습니다. 재미있는 점은 User-interactive이 Main Thread의 우선순위라고 쓰여있는 점입니다. 그럼 MainQueue도 Main Thread에서 동작하고 User-interactive Serial Queue도 Main Thread에서 동작하면 이 둘은 같을까요?

 

실험을 해보았습니다.

 

 

결과는 userInteractiveQueue는 MainThread에서 동작하지 않습니다. 즉, Main Queue는 UserInteractive의 우선순위를 갖지만, 

UserInteractive Queue라고 MainThread에서 동작하지는 않습니다. 블로그에서 흔히 UserInteractive는 MainThread에서 동작한다고 하는데 이는 틀린 말이라고 할 수 있습니다.

 

Custom Queues

 

https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2#toc-anchor-004

 

이런 글을 보게 되었습니다. Custom Queue가 Global Queue에서 실행된다,  구현 문서에 들어가면 내용을 확인할 수 있는데요

 

default target queue == default priority glboal concurrent queue.

 

결론만 말하자면 저 말은 맞고, GCD문서에 살짝 누락이 있습니다. Default-priority global concurrent queue는 2개가 존재합니다. overcommit된 queue, overcommit되지 않은 queue.

 

내부 root queue들의 코드, overcommit된 코드를 볼 수 있습니다. Root queue를 Global Queue로도 부르는것 같습니다. 
overcommit queue 설명 Overcommit 된 Queue는 시스템 리소스를 신경쓰지 않고 새로운 Thread를 만듭니다. (https://opensource.apple.com/source/libdispatch/libdispatch-1271.40.12/private/queue_private.h.auto.html)

 

 

아래의 실험으로 이를 증명할 수 있습니다.

import Dispatch

let serial = DispatchQueue(label: "serial")
let concurrent = DispatchQueue(label: "concurrent", attributes: .concurrent)

let DISPATCH_QUEUE_OVERCOMMIT = 2
let defaultOvercommit = __dispatch_get_global_queue(Int(QOS_CLASS_DEFAULT.rawValue), UInt(DISPATCH_QUEUE_OVERCOMMIT))
print(QOS_CLASS_DEFAULT.rawValue)
let serial_1 = DispatchQueue(label: "serial_1", target: serial)
let serial_2 = DispatchQueue(label: "serial_1", target: concurrent)

let group = DispatchGroup()

group.enter()
serial.async {
  dispatchPrecondition(condition: .onQueue(serial))
  dispatchPrecondition(condition: .onQueue(defaultOvercommit))
  print("on serial")
  group.leave()
}

group.enter()
concurrent.async {
  // Both preconditions are met
  dispatchPrecondition(condition: .onQueue(concurrent))
  dispatchPrecondition(condition: .onQueue(DispatchQueue.global(qos: .default)))
  print("on concurrent")
  group.leave()
}

group.enter()
serial_1.async {
  dispatchPrecondition(condition: .onQueue(serial))
  dispatchPrecondition(condition: .onQueue(defaultOvercommit))
  print("again on serial")
  group.leave()
}

group.enter()
serial_2.async {
  // Both preconditions are met
  dispatchPrecondition(condition: .onQueue(concurrent))
  dispatchPrecondition(condition: .onQueue(DispatchQueue.global(qos: .default)))
  print("again on concurrent")
  group.leave()
}

group.wait()

 

 

 

결과

 

실험의 내용을 설명하겠습니다.

__dispatch_get_global_queue함수를 이용해, flag에 0x2ull를 넣어 강제로 Default Overcommit Global Queue를 받아옵니다. 

dispatchPrecondition를 이용하여 현재 큐가 어디인지 검사합니다. (조건과 다르면 실행이 중단됩니다.) 결과 SerialQueue는 default overcommit Queue, ConcurrentQueue는 DispatchQueue.global(qos: .default)위에서 동작함을 확인할 수 있습니다. 참고로 DisaptchQueue.global은 항상 overcommit되지 않은 queue를 반환합니다.

 

그런데 ConcurrentQueue라면 이해가 가지만 Serial Queue가 어떻게 Concurrent Global Queue에서 Serial함을 보장받을 수 있을까요?

 

man dispatch_queue_create

 

매뉴얼에는 그렇게 구현되어 있다고만 써져있습니다. 오픈소스를 보면, 

 

concurrent 이면 width(queue에서 한번에 나올 수 있는 Task의 수로 이해했습니다.)가 MAX로 매핑되고, serial이면 width가 1개로 매핑됩니다. (https://opensource.apple.com/source/libdispatch/libdispatch-1271.40.12/src/queue.c.auto.html)

 

 

아래는 알아낸 전제조건입니다.

1. Serial Queue라고 해서 항상 동일한 스레드에서 실행됨이 보장되진 않습니다. (forums.swift.org/t/what-is-the-default-target-queue-for-a-serial-queue/18094/7)

2. Concurrent 이면 Width(Thread 수로 이해)가 MAX로 매핑되고, Serial이면 Width가 1개로 매핑됩니다.

3. Serial Queue는 반드시 작업의 순서가 보장됩니다.

4. Serial Queue는 Overcommit Global Queue에서 동작합니다.

5. Overcommit Global Queue는 항상 새로운 Thread를 만듭니다.

6. Root queue도 Concurrent queue입니다.

 

break Point를 통해 Root queue도 concurrent queue임을 알아냈습니다.

 

 

아래는 추론입니다.

이 전제조건을 가지고 추론하면, Custom Serial Queue를 선언하면  Overcommit Global Queue를 통해 실행되기 때문에 리소스에 상관없이 무조건 Thread가 하나 생깁니다.(많은 Thread의 생성은 퍼포먼스 저하를 가져올 수 있습니다.) Serial Queue는 Width가 1이기 때문에 Queue에서 항상 1개의 Task만 Overcommit Global Queue로 이동됩니다.(Serial Queue 내의 다음 Task는 앞서 나온 Task가 끝날 때까지 Overcommit Global Queue에 들어가지 않습니다.) Overcommit Global Queue는 Concurrent Queue이고 여기서 매칭 되는 Thread는 각각의 Task마다 리소스 가용성에 따라 변동될 수 있습니다.

 

생각보다 GCD 내부가 복잡하여 글이 길어졌습니다. Reference를 보시다가  Block이라는 표현이 나오면, Clousure로 이해하시면 될 것 같습니다. 2부에서는 DispatchWorkItem, DispatchGroup, DispatchSemaphore, Concurrency Problems에 대해서 적을 예정입니다.

 

References

Concurrency by Tutorials By Scott Grosch (www.raywenderlich.com/books/concurrency-by-tutorials/v2.0)

www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2

developer.apple.com/documentation/dispatch

stackoverflow.com/questions/19179358/concurrent-vs-serial-queues-in-gcd

developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html

developer.apple.com/documentation/dispatch/dispatchqueue

stackoverflow.com/questions/45912528/is-dispatchqueue-globalqos-userinteractive-async-same-as-dispatchqueue-main

developer.apple.com/videos/play/wwdc2015/718/

forums.swift.org/t/what-is-the-default-target-queue-for-a-serial-queue/18094

opensource.apple.com/source/libdispatch/libdispatch-1271.40.12/src/init.c.auto.html

opensource.apple.com/source/libdispatch/libdispatch-1271.40.12/src/queue.c.auto.html

opensource.apple.com/source/libdispatch/libdispatch-1271.40.12/src/queue_internal.h.auto.html

newosxbook.com/articles/GCD.html

JK님

maskkwon.tistory.com/159

www.programmersought.com/article/5978746266/