* GCD 포스팅은 2편으로 구성되어 있습니다.
1편은 여기에서 볼 수 있습니다.
Dispatch Queue 종류
Dispatch Queue는 세 가지 종류가 있습니다.
Main Queue, Global Queue, Custom Queue 인데요.
하나씩 알아보도록 합시다.
Main Queue
Main Queue는 Main Thread에서 작업을 보관하고 수행하는 Queue입니다.
Main Thread에서 동작하기 때문에 단 하나만 존재할 수 있고,
자연스럽게 Serial 특성을 갖습니다.
(Concurrent는 여러 Thread로 분산해야 하는데 Queue가 단 하나만 존재하므로 분산될 수가 없다)
Main Queue는 UI 업데이트를 담당하는데요.
화면 위에 UI를 그리는 작업을 Main Queue에서 담당하고 있습니다.
Main Queue가 아닌 다른 Queue에서 UI 작업을 하면 런타임 에러가 발생하니 주의해야 합니다.
그래서 이런 코드 많이 보셨을거에요.
DispatchQueue.global().async {
// (비동기 네트워크 작업) 이미지 다운로드
...
DispatchQueue.main.async {
// (UI관련) 다운로드한 이미지를 화면에 표시
}
}
Global Queue에서 UI 업데이트를 하면 안 되기 때문에
내부에 Main Queue로 UI 업데이트 코드를 보내는 것입니다.
왜 Main Queue에서만 UI를 업데이트해야만 하는걸까요?
UI도 여러 Queue에서 진행하면 빠르게 그릴 수 있지 않을까요?
정답은 아닙니다!
UIKit은 Thread-safe 하지 않습니다.
여러 Thread에서 접근할 때 데이터 안전을 보장하지 않는다는 의미인데요.
예를 들어, CollectionView에서 Cell을 지우는 Thread와 접근하는 Thread가 타이밍이 어긋날 경우
런타임 에러가 발생할 수 있습니다.
UIKit의 대부분 구성 요소들이 nonatomic(서로 연결)으로 구현되어 있고,
UIKit의 모든 속성들을 Thread-safe하게 디자인하는 것은 UIKit이 너무 방대하기 때문에 불가능하다고 합니다.
또한, iOS 에서는 Core Animation 프레임워크에 의해 표시되는데요.
Core Animation은 Core Animation Pipe line을 이용해 렌더링한다고 합니다.
여러 Thread에서 각각 렌더링을 진행하면 GPU 렌더링 작업 비용이 높아 성능상 좋지 않다고 하네요.
(글쓴이: 위 내용은 너무.... 어려워서... 쓰면서도 정확히 무슨 말인지 이해가 안 되네요... ㅠㅠ)
아무튼,
Main Queue는 단 하나만 존재할 수 있고, Serial 하며
UI 업데이트를 진행한다!는 것만 기억해주세요.
Global Queue
다음으로 알아볼 큐는 Global Queue입니다.
Concurrent 특성을 가진 Queue로 QoS(Quality of Service)에 따라 6개의 종류로 나뉩니다.
DispatchQueue.global().async {
}
기본은 이런식으로 작성하고,
DispatchQueue.global(qos: .utility).async {
}
이렇게 qos 옵션을 넣어 우선순위(작업의 중요도)를 설정할 수 있습니다.
Concurrent 특성에 따라 여러 Thread에서 작업이 분산되어 처리되기 때문에
작업의 순서가 일정하다는 것을 보장하지는 않지만
QoS를 지정함에 따라 어느정도 순서를 정해줄 수 있는 것이죠.
QoS의 종류와 실행 순서는 아래에서 알아보도록 하겠습니다.
Custom Queue
Custom Queue는 사용자가 Queue를 생성할 때 특성을 결정할 수 있습니다.
기본값으로는 Serial 특성을 가집니다.
1번에서 예시 코드로 사용했던
let queue = DispatchQueue(label: "queue")
이런게 Custom Queue 에요.
let concurrentQueue: DispatchQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
attribute를 추가하여 Concurrent하게 생성할 수 있습니다.
Quality of Service - QoS
QoS 종류
6개의 QoS는 아래와 같습니다.
1. userInteractive
사용자와 직접 상호작용 하는 작업 (ex. UI 업데이트, 애니메이션 등)
사용자의 행동에 대한 즉각적인 반응이 기대되지만 이를 Main Thread에서 처리하면 많은 load가 걸리는 작업들을 userInteractive에서 처리합니다. 바로 동작하는 것처럼 보이는 효과가 있습니다.
순식간에 끝나며 반응성과 성능에 중점을 둡니다.
2. userInitiated
클릭할 때 작업을 수행하는 것처럼 즉각적인 결과가 필요한 작업
userInterative보다는 오래 걸릴 수 있습니다.
몇 초 또는 그 이하의 시간이 걸리며 반응성과 성능에 중점을 둡니다.
3. default
가장 일반적인 작업.
QoS를 설정하지 않으면 default로 설정됩니다.
4. utility
progress bar처럼 길게 실행되는 작업 (ex. 데이터 다운로드)
사용자와 상호작용 하지 않으면서 오랜 시간 동안 작업을 해야할 때 사용합니다.
몇 초~몇 분이 걸리며 반응성, 성능, 에너지 효율성 간에 균형을 유지하는데 중점을 둡니다.
5. background
유저가 직접적으로 인지하지 않는 시간이 덜 중요한 작업 (ex. 동기화 및 백업)
백그라운드에서 사용자가 볼 수 없는 작업을 합니다.
분~시간처럼 상당한 시간이 걸리며 에너지 효율성에 중점을 둡니다.
6. unspecified
QoS 정보가 없음을 나타냄
환경 Qos(environmetal Qos)를 추론해야 한다는 단서를 시스템에 제공합니다.
QoS 설정하기
QoS는 Queue나 Task에 qos 옵션을 통해 설정할 수 있습니다.
let queue = DispatchQueue.global(qos: .background)
이렇게 Queue에 우선순위를 설정할 수도 있고
DispatchQueue.global().async(qos: .utility) {
}
이렇게 Task에 우선순위를 설정할 수도 있는데
DispatchQueue.global(qos: .background).async(qos: .utility) {
}
이렇게 Queue와 Task 둘 다 QoS를 설정할 수도 있습니다.
이때는 어떤 QoS로 동작이 될까요?
Task의 QoS > Queue의 QoS인 경우
Task의 작업이 있는 Queue에 동안 일시적으로 Queue의 QoS가 상승합니다.
QoS가 높은 Task가 Thread에 분배되어 Queue에서 사라지는 즉시 Queue의 우선순위는 다시 내려가요.
예를 들어,
let queue = DispatchQueue.global(qos: .background)
queue.async(qos: .utility) {
// utility로 동작
}
queue.async {
// background로 동작
}
이런 코드가 있을 때 background는 utility보다 우선순위가 낮습니다.
따라서 첫 번째 Task에서는 일시적으로 Queue의 우선순위가 utility로 상승하고
1번 Task가 Thread에 분배되면 다시 내려갑니다.
2번 Task는 따로 QoS를 설정하지 않았으므로 그대로 background 우선순위로 동작하게 됩니다.
Task QoS < Queue QoS인 경우
Task가 Queue의 QoS를 따라가게 됩니다.
let queue = DispatchQueue.global(qos: .utility)
queue.async(qos: .background) {
// utility로 동작
}
이렇다면 Task가 background로 설정될지라도
Queue의 QoS를 따라 utility로 동작하게 되는 것이죠.
QoS를 잘 설정하면 좋은 이유
QoS를 제대로 알아서 적절히 설정하면 좋은 이유가 뭘까요?
그 이유는 공식 문서에서 볼 수 있었습니다.
Accurately specifying appropriate QoS classes for the work your app performs ensures that your app is responsive and energy efficient.
QoS의 종류를 알아볼 때 반응성과 성능이 중점인 QoS와 에너지 효율성이 중점인 QoS가 있었습니다.
만약 성능이 중요하지 않다면 에너지 효율성이 높은 QoS를 지정하여
기기 에너지를 절약할 수 있겠죠?
이렇게 QoS는 앱의 성능 디테일 요소에 기여합니다!
아직 얼마나 차이가 나는지는 모르겠으나 알아두면 좋은건 확실한거 같네요!
경험이 쌓이면 꼭 다시 비교를 해봐야겠습니다 ㅎㅎ
QoS 실습하기
Queue의 종류와 QoS 종류에 대해 알아보았으니
QoS에 따라 어떻게 동작하는지 실습을 통해 알아봅시다.
일단 Queue 2개의 우선순위가 같을 때 어떻게 동작하는지 봅시다.
let queue1 = DispatchQueue(label: "Queue1", qos: .userInteractive)
let queue2 = DispatchQueue(label: "Queue2", qos: .userInteractive)
queue1.async {
for i in 1...5 {
print("[🐶] \(i)")
}
}
queue2.async {
for i in 100...105 {
print("[🌝] \(i)")
}
}
두 개의 Queue 모두 userInteractive 입니다.
결과는 어떨까요?
왔다리 갔다리 하면서 비등비등하게 출력되네요.
코드상으로는 Queue1이 먼저 등록이 되었지만
그렇다고 먼저 수행되는 것도 아니라는 것을 알았습니다.
이번에는 Queue1이 Queue2보다 우선순위가 더 높아요.
let queue1 = DispatchQueue(label: "Queue1", qos: .userInteractive)
let queue2 = DispatchQueue(label: "Queue2", qos: .utility)
이때 Task를 각각 돌리면 어떤 결과가 나올까요?
Queue1이 우선적으로 수행됩니다.
하지만 무조건 먼저 수행되는 것은 아닌 것을 볼 수 있습니다.
두 번째 사진은 Queue2가 Queue1보다 먼저 종료가 되었습니다.
이렇게 QoS는 절대적인 순서를 지정하는게 아니고
말그대로 우선적으로 수행하겠다는 것을 의미합니다.
헷갈리면 안 돼요!
마지막으로
Queue와 Task의 QoS가 다른 상황을 테스트 해봅시다.
let queue1 = DispatchQueue(label: "Queue1", qos: .utility)
let queue2 = DispatchQueue(label: "Queue2", qos: .utility)
queue1.async(qos: .userInteractive) {
for i in 1...5 {
print("[🐶] \(i)")
}
}
queue2.async {
for i in 100...105 {
print("[🌝] \(i)")
}
}
2개의 Queue 모두 utility로 설정하고
1번 Task에만 userInteractive로 설정해주었습니다.
그러면 위에서 알아본 이론대로라면 강아지 출력이 우선이 돼야겠네요.
하지만 결과를 보면 아니죠... ㅎ
왜 그럴까요? (정말 모름)
심지어
let queue1 = DispatchQueue(label: "Queue1", attributes: .concurrent)
queue1.async(qos: .userInteractive) {
for i in 1...5 {
print("[🐶] \(i)")
}
}
queue1.async {
for i in 100...105 {
print("[🌝] \(i)")
}
}
이렇게 같은 Queue의 Task에 우선순위를 적용해도
이렇게 아무 효과가 없답니다.
async 문서에는... 설정할 수 있다고 나와있는데... ㅠㅠ
심지어 WWDC에서도
소개하는 내용인데 왜 안 되는걸까요? ㅎㅎ;
하지만 DispatchWorkItem을 이용할 때는 또 달랐습니다.
let queue1 = DispatchQueue(label: "Queue1", attributes: .concurrent)
let item1 = DispatchWorkItem(qos: .userInteractive) {
for i in 1...5 {
print("[🐶] \(i)")
}
}
let item2 = DispatchWorkItem(qos: .utility) {
for i in 100...105 {
print("[🌝] \(i)")
}
}
queue1.async(execute: item1)
queue1.async(execute: item2)
DispatchWorkItem을 이용해 QoS를 설정해보았는데요.
이때는 QoS가 잘 적용이 되었습니다.
문서를 조금 더 찾아봐야겠지만
executing the block에 직접 qos를 적용하는건 아무런 의미가 없는게 아닌지!
하는 추측을 해봤지만 문서를 조금 더 찾아보고 내용을 추가하도록 하겠습니다...
GCD의 우선순위 역전 내용 더 자세히 보기
참고
https://developer.apple.com/documentation/dispatch/dispatchqos
https://developer.apple.com/documentation/dispatch/dispatchqueue/2016098-async
https://jeonyeohun.tistory.com/279
https://velog.io/@leeyoungwoozz/iOS-DispatchQueue.main.async
https://zeddios.tistory.com/520
https://zeddios.tistory.com/521
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.