GCD와 Dispatch Queue는 서로 관련이 있는 개념이다.
Dispatch, also known as Grand Central Dispatch (GCD), contains language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in macOS, iOS, watchOS, and tvOS.
먼저, 애플공식 문서에서 GCD 개념이다.
GCD는 멀티코어 시스템에서 동시성 실행을 제공하는 프로그래밍 언어 요소, 런타임 라이브러리 등이라고 합니다.
따라서 GCD와 Dispatch Queue는 엄밀히 말해 같은 개념이라고 말할 수 없다.
이러한 GCD의 개념으로 동시성 프로그래밍을 지원하는 스위프트의 API가 Dispatch Queue 이다.
자바나 C같은 언어를 보면, 스레드를 생성하기 위해서 명시적으로 스레드를 만들고 해야할 작업도 특정한 스레드에 지정해주어야 했습니다.
Swift에서는 Thread Pool의 관리를 개발자가 아닌 운영체제에서 관리를 해줍니다.
개발자는 실행할 Task를 생성하고 Dispatch Queue에 추가하기만 하면
GCD가 알아서 Task를 적절한 Thread에 배분하고 관리해줍니다.
이제 Dispatch Queue는 어떻게 사용하는지 알아보자.
DispatchQueue.{큐종류}(qos 옵션).{sync / async} {
Task
}
Dispatch Queue는 일반적으로 위 코드처럼 작성하여 사용할 수 있습니다.
개발자는 큐의 종류, qos 우선순위, sync / async만 설정하고 Task를 넣어주기만 하면 됩니다.
예를 들자면,
DispathQueue.main.async {
print("Hello world")
}
위 코드는
1. main: 메인 큐에서
2. async: 비동기로
3. { ... }: print("Hello world") 이라는 Task를 처리하겠다.
라는 의미이다.
Serial과 Concurrent는 DispatchQueue에서 사용되는 큐의 실행 방식을 나타내는 개념입니다.
Serial Queue와 Concurrent Queue는 주로 작업의 실행 순서와 동시 접근에서 차이점을 가지고 있습니다.
Serial Queue은 큐에 추가된 작업이 순차적으로 실행되는 큐입니다.
한 번에 하나의 작업만 실행되며, 이전 작업이 완료되어야 다음 작업이 시작됩니다.
Serial Queue는 일반적으로 데이터의 일관성과 동시 접근에 문제가 발생할 수 있는 작업에 사용됩니다.
let numbers = [1, 2, 3, 4, 5]
let serialQueue = DispatchQueue(label: "serial")
for i in (0...3) {
serialQueue.async {
for number in numbers {
print("\(i) - \(number)")
}
}
}
위와같이 Serial Queue를 만들어 실행하게 된다면
0 - 1
0 - 2
0 - 3
0 - 4
0 - 5
1 - 1
1 - 2
1 - 3
... 과
같은 실행결과를 받아볼 수 있는데, Task 작업이 순착적으로 일어나는 걸 확인할 수 있습니다.
Concurrent Queue는 큐에 추가된 작업들이 병렬로 실행될 수 있는 큐입니다. 동시에 여러 작업이 실행될 수 있으며, 작업의 순서는 보장되지 않습니다.
Concurrent Queue는 작업을 병렬로 처리해야 할 경우에 사용됩니다.
let numbers = [1, 2, 3, 4, 5]
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
for i in (0...3) {
concurrentQueue.async {
for number in numbers {
print("\(i) - \(number)")
}
}
}
위와 같이 Concurrent Queue를 만들어 실행해보면
0 - 1
1 - 1
2 - 1
1 - 2
1 - 3
1 - 4
0 - 2
3 - 1
1 - 5
... 와 같이
실행할 때마다 출력 결과가 다른걸 볼 수 있습니다. 이것은 작업의 순서가 보장되지 않는다는 것을 알 수 있습니다.
Dispatch Queue를 사용할 때 우리는 세 종류의 큐를 선택하여 사용할 수 있습니다.
Main Queue
Main Queue는 앱의 주 메인 스레드에서 실행되는 큐입니다.
주로 UI 업데이트와 사용자 상호작용 관련 작업을 처리하는 데 사용됩니다.
Main Queue는 Serial Queue의 일종으로, 작업들이 순차적으로 실행됩니다.
주로 메인 스레드에서 비동기적으로 실행해야 하는 작업들을 추가할 때 사용됩니다.
Global Queue
Global Queue는 앱 전역에서 사용할 수 있는 큐입니다.
여러 개의 Concurrent Queue로 구성되어 있으며, 각 큐는 다른 QoS(Quality of Service) 수준을 가집니다.
각 큐는 병렬로 작업을 실행할 수 있으며, 시스템에서 자동으로 관리됩니다.
Global Queue는 주로 백그라운드 작업이나 비동기적인 작업들을 처리하는 데 사용됩니다.
Custom Queue
Custom Queue는 사용자가 어떤 특성의 큐로 Dispatch Queue를 생성할지 결정할 수 있도록 해줍니다. 기본값으로는 Serial을 가지고있지만, 생성시에 attributes 인자를 통해 concurrent로 변경하는 것이 가능합니다.
위에서 말했던 Global Queue나 Cutom Queue에서는 QoS를 설정할 수 있다.
QoS(Quality of Service)는 작업의 우선순위를 지정하는 데 사용되는 개념입니다. QoS는 작업이 얼마나 중요하고 어떤 종류의 작업인지를 나타내며, 시스템은 이 정보를 활용하여 리소스 할당과 작업 스케줄링을 결정합니다.
QoS는 다섯가지의 종류가 있습니다.
userInteractive
사용자와 직접적인 상호작용을 하는 작업에 사용하기 좋습니다. 예를 들어 애니메이션이나 앱의 인터페이스를 업데이트 하는 등 UI 관련 이벤트를 처리할 때 사용합니다. 유저와 상호작용을 하는 작업들을 처리하다 보니 작업을 처리하는데 드는 소요시간은 상당히 짧은 편입니다.
userInitiated
사용자가 initiated 한 뒤로 즉각적인 처리가 이루어져야 하는 작업에 대해 사용합니다. 작업이 끝날 때까지 유저가 인터렉션을 할 수 없습니다. 따라서 사용자에게 즉각적인 결과를 주어야 하지만 작업이 끝난 뒤에나 의미가 있다면 userInitiated qos를 사용하는 것이 효과적입니다. 예를 들어 파일을 열거나, 유저가 유저인터페이스에서 무엇인가를 클릭한 뒤의 액션을 처리하거나 api로부터 데이터를 로딩하는 작업 등을 수행할 수 있습니다. 소요시간은 몇초나 그것보다 적게 든다고 알려져 있습니다.
default
qos를 선택하지 않으면 기본값으로 선택되는 qos입니다. userInteractive 와 userInitiated 보다는 중요도가 낮고, utility와 background보다는 높은 중요도를 갖습니다.
utility
즉각적인 결과가 필요하지 않은 경우에 사용할 수 있습니다. 예를 들어 데이터 다운로드 및 불러오거나 progress indicator와 함께 길게 실행되는 작업. 계산, I/O, networking 등에서 사용합니다. 소요시간은 수초 에서 수분 단위로 든다고 알려져 있습니다.
background
유저에게 직접적으로 보이지 않고 백그라운드에서 처리되는 작업의 경우에 사용하는 qos입니다. 데이터 미리 가져오기, 로컬 DB에 데이터를 저장하는 작업, 백업, 동기화 등의 우선순위가 높지 않은 작업에 사용합니다. 속도보다는 에너지의 효율성을 고려하는 방식입니다. 아이폰의 저전력모드일 때 작업이 일시중단됩니다. 소요 시간은 수분에서 수시간이 걸린다고 알려져 있습니다.
Sync와 Async는 DispatchQueue에서 작업을 실행하는 방식을 나타내는 개념입니다.
Sync와 Async는 작업을 실행하고 결과를 반환받는 방식에 있어서 차이점이 있습니다.
Sync는 작업이 완료될 때까지 대기하며 결과를 반환받지만,
Async는 작업을 큐에 추가하고 결과를 기다리지 않고 다음 코드로 진행됩니다.
let queue = DispatchQueue(label: "ex-sync")
print("Before")
queue.sync {
for i in 1...5 {
print("Task \(i)")
sleep(1)
}
}
print("After")
위 코드처럼 Sync로 작업을 진행 시킨다면 아래와 같은 결과를 볼 수 있다.
Before
Task 1
Task 2
Task 3
Task 4
Task 5
After
이 처럼 Sync는 작업이 완료될 때까지 대기하며 결과를 반환하는 것을 확인할 수 있습니다.
let queue = DispatchQueue(label: "ex-Async")
print("Before")
queue.async {
for i in 1...5 {
print("Task \(i)")
sleep(1)
}
}
print("After")
위 코드처럼 Async로 작업을 진행 시킨다면 아래와 같은 결과를 볼 수 있습니다.
Before
After
Task 1
Task 2
Task 3
Task 4
Task 5
이 처럼 Async는 결과를 기다리지 않고 다음 코드를 진행하는 것을 볼 수 있습니다.
Sync는 작업이 완료될 때까지 대기하므로 작업이 끝날 때까지 해당 스레드 또는 큐에서 블로킹되는 경우가 발생할 수 있습니다. 이로 인해 메인 큐에서 sync 메서드를 호출하면 UI가 응답하지 않을 수 있으므로 주의해야 합니다.
반면에 Async는 작업을 큐에 추가하고 즉시 반환되므로 다음 코드로 진행됩니다.
작업은 비동기적으로 실행되며, 작업이 완료된 시점에 콜백 또는 완료 핸들러를 통해 결과를 처리할 수 있습니다.
이를 통해 비동기 작업을 동시에 실행하거나 블로킹되지 않고 UI의 응답성을 유지할 수 있습니다.