* GCD 포스팅은 총 2편으로 작성되었습니다.
1편에서는 GCD가 무엇인지, serial/concurrent, sync/async에 대해 알아봅니다.
2편에서는 Dispatch Queue의 종류와 특성에 대해 알아볼 계획입니다.
GCD(Grand Central Dispatch)란?
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(Grand Central Dispatch)는 멀티 코어와 멀티 프로세싱 환경에서
최적화된 프로그래밍을 할 수 있도록 애플이 개발한 기술입니다.
동시성 프로그래밍
메인 Thread에만 Task(작업)가 몰리면 효율이 감소합니다.
예를 들어, 연산을 하는 작업, 네트워킹 작업, UI를 그리는 작업을 모두 메인 Thread가 한다면
처리 시간이 지연되면서, 일명 렉 걸리는 앱이 될 것입니다.
그래서 나온 개념이 동시성 프로그래밍입니다.
여러 Task를 여러 개의 Thread에 분배해서 처리하자는 것인데요.
GCD의 "멀티 코어와 멀티 프로세싱 환경"이 바로 이를 의미하는 것입니다.
보통 동시성 프로그래밍은 까다롭습니다.
C언어를 생각하면 Thread를 개발자가 직접 만들고 지정하고 실행시켜야 했죠.
Swift에서는 Thread Pool의 관리를 개발자가 아닌 운영체제에서 관리하기 때문에
간편하게 Task를 비동기적으로 사용할 수 있습니다.
개발자는 실행할 Task를 생성하고 Dispatch Queue에 추가하기만 하면
GCD가 알아서 Task를 적절한 Thread에 배분하고 관리해줍니다.
Dispatch Queue란?
Task의 실행을 관리하는 객체로
앱의 Thread에서 Task를 동기적으로(Serial), 혹은 비동기적으로(Concurrent) 실행하게 도와줍니다.
GCD의 설명과 크게 다르지 않죠?
Dispatch Queue는 GCD에서 사용하는 Queue이기 때문이에요. (Queue에 대해서는 여기서 확인해 주세요.)
Task들을 Dispatch Queue에 넣어놓고 하나씩 Thread에 배분하는 것이죠.
많은 설명에서 Dispatch Queue == GCD라고 하기도 하는데요.
의미는 같으나 완전히 똑같은 것은 아니라는 점 기억해 주세요.
DispatchQueue는 Queue의 종류와 처리 방식을 설정할 수 있습니다.
DispatchQueue.{Queue 종류}(.{qos 옵션}).{sync/async} {
//순차적으로 진행되는 하나의 작업 단위
}
Queue의 종류를 선택하고 선택적으로 qos 우선순위를 설정한 뒤
sync로 처리할지 async로 처리할지 설정합니다.
예를 들어,
DispatchQueue.global().async {
print("HI!")
}
이라고 한다면
- global() : Dispatch Queue 중 global Queue에서
- async : 비동기로
- { ... } : print("HI!")를 수행하는 Task를 처리하겠다
는 의미입니다.
Dispatch Queue에 대해 더 알아보기 전에!
Serial과 Concurrent, sync와 async에 대해 먼저 알아봅시다.
Serial(직렬) vs Concurrent(병렬/동시)
Serial(직렬)
Serial은 Dispatch Queue에 등록된 작업을 하나의 Thread에서만 처리하는 방법입니다.
그렇기 때문에 항상 일정한 작업 순서를 보장합니다.
Task1, Task2, Task3을 순서대로 넣었을 때,
Task1이 종료된 후 Task2가 수행되고, Task2가 종료되면 Task3이 수행 시작합니다.
따라서 Task가 순차적으로 수행되어야 할 때,
Task의 시작과 종료에 대한 순서 예측이 필요할 때 Serial하게 처리하면 됩니다.
Concurrent(병렬/동시)
Concurrent는 Dispatch Queue에 등록된 작업을 여러 개의 Thread에 분배해서 처리하는 방법입니다.
Queue의 특성상 순차적으로 분배되어 실행되긴 하지만, 끝나는 순서는 정확히 알 수 없습니다.
위에서 동시성 프로그래밍에 대해 잠깐 설명했는데요.
동시성 프로그래밍 방식이 바로 Concurrent 처리 방식입니다.
Task1, Task2, Task3을 순서대로 넣었을 때,
Task1, Task2, Task3이 각각의 Thread에서 "동시에" 수행되고
실행 시간이 짧은 Task부터 먼저 종료됩니다.
Concurrent 방식은 여러 Task가 동시에 수행되기 때문에
전체 수행 시간이 Serial보다 적습니다.
따라서 Task가 순차적일 필요가 없고 빠르게 처리하고 싶다면
Concurrent하게 처리하면 됩니다.
비교 실습
Serial과 Concurrent에 대한 간단한 실습을 해봅시다. (진짜 간단함 ㅎ;)
Serial Queue를 먼저 만들어봅시다.
var numbers: [Int] = [1, 2, 3, 4, 5]
let serialQueue: DispatchQueue = DispatchQueue(label: "serial")
for i in 0..<3 {
serialQueue.async {
for number in numbers {
print("[\(i)] \(number)")
}
}
}
DispatchQueue는 기본적으로 serial 하므로
아무 옵션을 주지 않으면 Serial Queue가 됩니다.
위 코드는 항상 실행 결과가 동일합니다.
0번 째 for문이 종료되면 1번이 수행되고 그 다음 2번이 수행됩니다.
1, 2, 3, 4, 5가 순차적으로 나오는 것으로
Dispatch Queue로 보낸 Task 작업도 순차적으로 수행되는 것을 알 수 있습니다.
Concurrent Queue를 만들어 봅시다.
DispatchQueue를 Concurrent하게 만들기 위해서는 옵션을 하나 줘야합니다.
var numbers: [Int] = [1, 2, 3, 4, 5]
let concurrentQueue: DispatchQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
for i in 0..<3 {
concurrentQueue.async {
for number in numbers {
print("[\(i)] \(number)")
}
}
}
attributes에 concurrent 옵션을 주어서 Concurrent Queue를 만들었습니다.
수행 결과는 Serial과 무슨 차이가 있을지 봅시다.
총 세 번 실행 시켜 봤습니다.
모든 결과가 다른 것을 볼 수 있습니다.
1번과 3번 결과는 1번 Task가 먼저 종료됐지만,
2번 결과는 2번 Task가 먼저 종료됐네요.
이렇게 Concurrent는 어떤 Task부터 끝날지 알 수가 없습니다.
Serial과 Concurrent의 수행 시간을 비교해볼까요?
위 코드에서 반복 횟수만 100회로 진행했을 때
수행 시간을 측정해보았습니다.
Concurrent가 약 9배 더 빠른 것을 볼 수 있습니다.
아마 반복 횟수가 늘수록 이 차이는 더 커질 것입니다.
이제 확실히 Serial와 Concurrent의 차이와 역할을 아시겠죠?
Sync(동기) vs Async(비동기)
//not real code
Task1 {
//1초 걸리는 작업
}
Task2 {
//2초 걸리는 작업
}
Task3 {
//3초 걸리는 작업
}
print("HI!!")
Sync
Sync는 Queue에 작업을 등록하고 해당 작업이 완료될 때까지 더이상 코드가 진행되지 않습니다.
위 코드를 sync로 실행 시켰을 때, 6초 뒤에 HI!!가 출력됩니다.
왜냐면 Task1이 종료된 후 Task2가 수행되고, Task2가 끝나면 Task 3이 수행되고
Task3이 끝난 뒤에야 HI!!가 출력되기 때문입니다.
sync한 serial Queue를 구현해보았습니다.
let queue = DispatchQueue(label: "queue")
queue.sync {
print("Task 1")
}
queue.sync {
print("Task 2")
}
queue.sync {
print("Task 3")
}
print("HI!!")
각 Task가 순차적으로 동작합니다.
Serial + Sync Queue 이기 때문에 항상 같은 순서로 출력됩니다.
Async
Async는 해당 작업을 Dispatch Queue에 보내기만 하고 바로 다음 작업으로 넘어갑니다.
그래서 위 코드를 async로 실행 시켰을 때, 즉시 HI!!가 출력됩니다.
HI!!가 출력이 된 후 각각의 Task가 종료될 거에요.
async한 serial Queue를 구현해보았습니다.
let queue = DispatchQueue(label: "queue")
queue.async {
print("Task 1")
}
queue.async {
print("Task 2")
}
queue.async {
print("Task 3")
}
print("HI!!")
Task를 Queue에 보내기만 하고 바로 다음 코드를 수행하기 때문에 HI!! 가 먼저 출력될 때도 있습니다.
Serial Queue이기 때문에 Task 1, 2, 3은 항상 같은 순서로 동작하지만,
"HI!!"는 같은 Queue가 아니기 때문에 Task들의 종료 시간에 따라 출력 순서가 달라질 수 있습니다.
Async로 동작할 때 각각의 Task가 종료되었다는 것을 어떻게 알지?
하는 의문이 있으실텐데요.
이럴 때 사용하는 것이 바로 completion handler, rxswift, combine 입니다.
이번 포스팅에서 다루지는 않지만 궁금하실까봐...ㅎ
Async와 Concurrent의 차이
Async와 Concurrent가 무슨 차이가 있는지 헷갈리실 수 있습니다.
결과는 비슷할지 몰라도 완전히 다른 개념이에요!
sync와 async는
작업을 보내는 시점에서 기다릴지 말지 결정하는 것이고,
serial과 concurrent는
Queue에 보내진 작업들을 한 개의 Thread로 보낼 것인지 여러 Thread로 보낼 것이 결정하는 것입니다.
절대 절대 같다고 생각하면 안 되는 개념이에요.
Queue 동작 처리 조합
Queue의 동작에 대해 네 가지를 알아봤습니다.
- Sync (동기) / Async (비동기)
- Serial (직렬) / Concurrent (동시)
이들을 조합하면 총 네 가지 조합이 나오는데요.
각각 어떤 특징이 있는지 알아볼게요.
Serial + Sync
메인 Thread의 작업 흐름이 Queue에 넘긴 Task가 끝날 때까지 멈춰있고 (sync)
넘겨진 Task는 모두 하나의 Thread에 보내지기 때문에 들어간 순서대로 처리됩니다. (serial)
코드로는
let serialQueue: DispatchQueue = DispatchQueue(label: "serial")
serialQueue.sync {
...
}
이렇게 표현할 수 있습니다.
Serial + Async
메인 Thread의 작업 흐름이 Task를 Queue에 넘기자마자 반환되고 (async)
넘겨진 Task는 하나의 Thread에 보내지기 때문에 들어간 순서대로 처리됩니다. (serial)
let serialQueue: DispatchQueue = DispatchQueue(label: "serial")
serialQueue.async {
...
}
Concurrent + Sync
메인 Thread의 작업 흐름이 Queue에 넘긴 Task가 끝날 때까지 멈춰있고 (sync)
넘겨진 Task는 여러 개의 Thread에 분배되기 때문에 다른 작업들이 끝나지 않았더라도 실행됩니다. (concurrent)
let concurrentQueue: DispatchQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync {
...
}
Concurrent + Async
메인 Thread의 작업 흐름이 Task를 Queue에 넘기자마자 반환되고 (async)
넘겨진 Task는 여러 개의 Thread에 분배되기 때문에 다른 작업들이 끝나지 않았더라도 실행됩니다. (concurrent)
let concurrentQueue: DispatchQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.async {
...
}
데드락 주의
Dispatch Queue를 사용할 때 데드락이 발생할 수 있습니다.
Main Thread에서 main.sync
DispatchQueue 공식 문서에 무려 Important라고 강조까지 한 부분인데요.
main queue를 sync로 동작시키면 데드락이 생길 수 있다고 합니다.
1. Main DispatchQueue에서 Sync를 호출하면 메인 Thread는 그대로 진행을 멈춥니다.
등록한 작업이 끝날 때까지 기다려야 하기 때문이죠.
2. 동시에 Queue에 등록된 작업을 메인 Thread에 할당하죠.
하지만 1번에서 메인 Thread는 진행을 멈췄습니다.
따라서 등록된 작업은 시작하지 못하고 메인 Thread는 시작하지도 않은 작업을 종료되길 기다리는
데드락이 발생한 것입니다.
특수한 경우에는 main.sync를 사용할 수 있는데요.
백그라운드 Thread에서 특별한 순서에 맞게 UI가 업데이트 되어야 하는 상황입니다.
DispatchQueue.global().async {
print("before textLabel update" )
DispatchQueue.main.sync {
print("Test1")
}
print("after textLabel update Test1")
DispatchQueue.main.sync {
print("Test2")
}
print("after textLabel update Test2" )
}
// before textLabel update
// Test1
// after textLabel update Test1
// Test2
// after textLabel update Test2
출처: https://zeddios.tistory.com/519 [ZeddiOS:티스토리]
위 코드는 main.sync를 했음에도 제대로 동작이 됩니다.
무조건 main.sync를 쓰지 말아야지! 가 아니라
웬만하면 안 쓰는데 특정 상황에서는 가능하구나로 받아들여주시면 감사하겠습니다.
같은 Serial Queue에서 sync
같은 Queue에서 같은 Queue로 sync를 보내서도 안 됩니다.
이에 대한 내용은 sync 공식 문서에서도 다루고 있는데요.
Calling this function and targeting the current queue results in deadlock.
라고 말하고 있습니다.
Serial Queue 안에서 같은 Serial Queue로 sync 작업을 하게 되면
위에서 설명한 것과 같은 원리로
데드락이 발생합니다.
내부 sync를 async로 변경해주면
let queue = DispatchQueue(label: "queue")
queue.sync { // 1
for index in 1...5 {
print("Hello \(index)")
}
queue.async { // 2
print("!!")
}
print("##")
}
정상적으로 동작합니다.
또한 Serial Queue가 아닌 Concurrent Queue에서도
정상적으로 동작합니다.
let queue = DispatchQueue(label: "queue", attributes: .concurrent)
queue.sync { // 1
for index in 1...5 {
print("Hello \(index)")
}
queue.sync { // 2
print("!!")
}
print("##")
}
마무리
오늘은 GCD와 sync/async, serial/concurrent에 대해 알아보았는데요.
비동기에 대한 개념이 잡힌 상태에서 봐도 참 헷갈리는 내용이었습니다 ㅎㅎ;
어렵고 헷갈리는 내용이지만 반드시 알아야하는 중요한 개념이기 때문에
포스팅하기 전 해당 내용에 대해 많이 조사했는데요.
그럼에도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다.
다음 포스팅에서는 Dispatch Queue의 종류와 특성에 대해 알아보도록 하겠습니다.
감사합니다!
참고
https://developer.apple.com/documentation/DISPATCH
https://developer.apple.com/documentation/dispatch/dispatchqueue
https://www.boostcourse.org/mo326/lecture/16916?isDesc=false
https://jeonyeohun.tistory.com/279
https://magi82.github.io/gcd-01/
https://zeddios.tistory.com/519
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.