본문 바로가기

iOS/Concurrency

[Swift] Hi GCD(GrandCentralDispatch). GCD's concept deep dive!!! | No3. GCD

 

안녕하세요. 

Swift의 concurrency를 공부하며 알게 된 개념을 정리하려고 합니다.

GCD 관련 포스트 정리.

Basic concepts before studying gcd

  • task
  • concurrency vs serial
  • synchronous vs asynchronous
  • Thread
  • Thread pool
  • dispatchQueue
  • GCD's sync, async func

 

 

Task

수행해야 할 작업들의 추상적인 개념을 의미합니다. ex) closure, block object, function etc...

 

Concurrency

특정한 시간대의 task가 prev task의 작업 완료와 상관없이 곧바로 실행되는 작업을 뜻합니다. 하지만 먼저 시작된다고 먼저 끝난다는 보장은 없습니다. task마다 크기가 다르기 때문입니다. 

https://www.kodeco.com/28540615-grand-central-dispatch-tutorial-for-swift-5-part-1-2

 

아래의 Serial과 다르게 이전 task의 작업 완료 여부와 상관없이 다음 task가 곧바로 실행될 수 있습니다. queue안에 있는 task들은 순서가 없기에 누가 먼저 시작될지 모릅니다. 동시에 수행되는 것은 맞습니다. task1, task2, task3은 동시에 수행된다. 동시성과 병렬성에 관한 개념을 살짝 알고 가면 좋을 것 같습니다.(concurrency and parallelism에 관한 포스트) 

 

Serial

주어진 시간에 한 가지의 task를 수행합니다. 특정한 시간대의 task가 시작되기 위한 조건이 있습니다. 반드시 prev task가 작업을 완료해야 다음 task가 실행됩니다.

https://www.kodeco.com/28540615-grand-central-dispatch-tutorial-for-swift-5-part-1-2

 

Serial은 루프(map,forEach,for-in)를 생각하면 좋습니다. 순차적인 실행입니다. 

 

Synchronous

줄여서 sync. 특정 task를 sync로 선언 하고 프로젝트를 실행했습니다. 해당 task가 실행될 차례가 온다면 task가 끝날 때 까지기 다렸다가 다음 라인으로 넘어갑니다. Task를 sink로 호출했다면 해당 task가 실행될 때 task가 끝나기를 꼭 기다립니다.

 

task1 생일 날 축하 노래 -> 촛불을 전부 끄기!

task2는 케이크를 자르는 행동

 

task1은 sync로 선언했고 task1 바로 아래에 task2를 작성하고 실행을 했다고 가정하겠습니다. task1을 실행할 차례가 된다면 task1이 끝날 때까지 다른 것을 할 수 없습니다. 생일을 맞이한 사람이 촛불을 직접 꺼야만 합니다.

 

시작했다면 반드시 끝을 봐야하는 성격을 갖고 있습니다.

 

Asynchronous

synchronous의 반대 개념입니다. 위에서 말한 sync는 Thread가 synchronous 한 task를 수행하게 된다면(해당 코드 라인을 실행하게 된다면) task가 끝날 때까지 기다려야 합니다. 반대로 async는 Thread가 asynchronous 한 task를 수행할 때! 해당 task가 끝날 때까지 기다리는 게 아닙니다. 일단 즉시 reutrun 합니다.

 

Thread가 asynchronous한 task를 호출하게 된다면 task를 실행하지 않고 다시 thread로 보냅니다. 했갈리면 안 될 정말 중요한 개념입니다. "실행하지 않고 Thread로 다시 보낸다! Thread가 기억했다 결국 언젠간 다시 실행될 것이다!"

 

 

async의 개념에 대해 확실하게 알아야 합니다. 단어의 뜻과 같이 async. 때가 맞지 않게!!! 원래 시작했었어야 했는데 나중에 task를 호출하는 것입니다. DispatchQueue.(global or main).async 는 concurrenct, serial과는 다른 개념입니다. 

 

Synchronous vs asynchronous

sync와 async의 차이점은

 

sync: Thread야 날 무시하지 말고 시작과 동시에 끝날 때까지 날 지켜봐 줘!! 다른 거 실행하지 마

async: Thread야 일단 async task 건너뛰고 Thread에서 호출해야 할 것들 호출해. 그러고 나서 나중에 task 호출해 줘.

("밥 한 끼 하자"는 약속은 곧바로일 수도 있고 1년 뒤 일수도 있고 다음 주일수도 있고 내일일 수도 있고 모릅니다.)

 

Thread

이전 포스트에서 잠깐 다뤘습니다. 특징을 간략하게 소개하자면 CPU는 multi core입니다. 즉 여러 thread를 사용할 수 있습니다. Swift는 multiThreading방식을 사용해 여러 개의 thread를 동시에 사용할 수 있습니다. 특정 process내 code, data, heap영역은 공유하고 thread는 Stack영역만 갖고 있습니다. 각 thread는 하나의 task를 처리할 수 있습니다. 더 자세한 건 운영체제에서..

 

쓰레드는 시간이 걸리는 task를 수행할 때 효율적으로 사용됩니다. 코드의 기본적인 실행은 main thread에서 실행됩니다. main queue는 단순히 main thread를 통해 코드를 실행시켜 주는 게 아닙니다. run loop와 함께 동작되며 run loop의 다른 이벤트를 처리를 같이 도와줍니다. 앱이 초기 실행될 때 UIApplication 인스턴스는 main Thread에 붙습니다. UIApplication은 맨 처음에 인스턴스화됩니다. run loop를 포함해 main event loop 설정, 여러 event를 감지합니다. 왜냐면?

https://developer.apple.com/documentation/uikit/uiapplication

UIResponder 타입이기 때문입니다. 사실 UIResponder에 의해 UIView에서 일어나는 모든 사용자의 event 탐지를 할 수 있습니다.

 

요약: cpu는 multi core이다. 여러 thread를 동시에 실행할 수 있습니다. 특히 main thread는 UIApplication과 run loop를 통해 사용자의 이벤트 발생도 감지하고 이에 맞는 delegate를 호출할 수 있습니다. 그와 동시에 다른 블럭 단위의 코드를 실행합니다. 시작과 동시에 실행되고 있는 것은 main Thread입니다. 그래서 엄청 중요합니다.

시간이 엄청 걸리는 task를 실행하게 된다면 main queue의 특성답게 serial( 다음 일을 하기 위해선 이전 일을 완료해야 한다.)로 인해 시간이 엄청 걸리는 task만 수행합니다. -> 사용자의 터치 이벤트에 의한 동작에 대한 delegate 호출 작업이 느려집니다. Thread를 사용한다면 여러 개의 작은~큰 작업을 main Thread와 분담해 사용할 수 있습니다. multi core resource를 효율적으로 사용하기 때문에 성능 향상 가능합니다.

 

Thread Pool

마찬가지로 이전 포스트에서 잠깐 다뤘습니다. 간략하게 설명하겠습니다. multi core는 여러 thread를 사용할 수 있습니다. swift에서 한 개 ~ 여러 개의 NSThread를 구현하게 된다면 task가 queue에 enqueue됬을 때, 어느 Thread에 할당 -> 실행 -> thread 해제 해야 할지 관리를 해야 합니다. Thread에 특정 task를 맡을 수 있는 queue를 구현해야 합니다. queue와 Thread들을 묶어 task의 submit, 특정 thread를 통한 task 실행을 담당하는 게 thread pool 입니다.

 

dispatchQueue

"어 여태까지 DispatchQueue.main.async가 비동기? concurrency로 알고 있었는데 그게 아닌가?"

 

네 아닙니다.

 

asynchronous는 실행해야 할 때 실행하지 않는 반면 sync는 실행하는. 예를 들어 어려운 과제가 주어지면 바로 시작해서 해결할 때까지 이틀 연속 밤새서 하는 스타일 = sync. 아니다!! 일단 과제가 주어졌지만 마감기한이 1주일 남았다. 나중의 나에게 맡긴다 == async 스타일로 정의할 수 있습니다.

 

그렇다면 dispatchQueue는 무엇일까요? Thread가 함수 실행도 다하는데 도대체 무엇을 할까요? 자료구조에서 나오는 queue의 특징을 갖고 있습니다. FIFO(선입선출: 먼저 들어온 자가 먼저 나간다)

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

 

DispatchQueue는 task의 수행을 관리하는 object입니다. class는 참조 타입을 의미합니다. task의 수행을 concurrentrly 또는 serially적으로 할 수 있습니다. task의 수행을 동시에 또는 일련의 순서대로 수행할 수 있게 관리합니다. task의 수행? task의 수행은 synchronously or asynchronously 하게 할 수 있습니다. 

 

dispatch queue가 task를 수행하는 방법은 thread pool에 task를 dispatch(발송)하는 것입니다.

thread pool은 누가 관리하는가? Apple의 system의 한 부분에서 관리합니다.

 

task를 수행하는 Thread는 언제 실행되는가? Apple's operating system's schedular 마음입니다. 언제, 어떻게, 어떤 조건에 의해

task가 실행될지는 system 마음입니다.

 

DispatchQueue는 thread-sfae입니다. 즉 multiple threads로부터 동시 접근이 가능합니다.

 

 

DispatchQueue는 3가지 종류가 있습니다.

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

  • main queue: serial
  • global queue: concurrent
  • custom queue: serial or concurrent

 

위에서 설명했지만 mainQueue는 serial입니다. main thread에서 특정 task가 실행해야 한다면 전제조건은 그 task의 이전 task가 끝나야 한다는 것입니다. thread에서 실행중인 task는 단 한 개 존재합니다. 

 

global queue는 concurrent입니다. 왜냐면 위에 Thread에서 설명했지만 시간이 오래 걸리는 작업을 mainThread와 concurrent 수행하기 위해서입니다. QoS에 따라 여러 개의 state에 따른 priority 설정이 가능합니다. main thread는 단 한개입니다. 이 한개로 모든 task를 한 Thread에서 관리한다면? no good.. multi core를 제대로 활용하지 않는 것입니다. (여러 core를 활용하기 위해 global queue와 같은 영역에서 Thread를 필요로 하면 resource를 사용해 Thread도 많이 생성했다 해제했다 등 main Thread의 부담을 덜어줍니다.)

 

custon queue는 init() 메서드를 구현함으로 사용이 가능합니다. 디폴트는 serial입니다. attributes를 통해 concurrent 큐로 설정할 수 있습니다.

 

GCD's sync func

https://developer.apple.com/documentation/dispatch/dispatchqueue/2016081-sync

 

dispatchQueue의 함수 sync입니다. 클로저 이름은 work입니다. 즉 queue에 work를 dispatch 할 때 dispatch 타입은 wrok인자 값인 () throws -> T 타입입니다.

 

앞에 언급해 온 task들은 () throws -> Void 타입입니다. () -> Void란 클로저의 유형인데 함수로 비유하자면 이름은 없고 매개변수(레이블)도 없고 반환 타입은 Void입니다. 여기서 끝이 아니라 사실 () throws -> T. throws가 있습니다. 클로저 안에서 발생한 에러도 던질 수 있습니다. (work는 dispatchWorkItem과 관련이 있습니다. 나중에 설명하려고 합니다.)

 

// 기본
dispatchQueue.global().sync(execute: { print("hello")})
// 클로저 특징 축약
dispatchQueue.global().sync{ print("hello") }

 

task 1을 synchronous로 코드를 작성했다면, Queue에서 enqueue 된 task 1은 thread에서 호출해서 task 1 안에 있는 내용을 실행합니다. task 1이 실행 중이라면 task 1의 실행 구문을 벗어나 아래의 task 2를 실행하지 않고 queue는 해당 task 1가 끝날 때까지 대기했다가 끝나면 task 2를 호출하면서 실행합니다. sync type task는 Thread는 해당 task만 실행할 수 있다는 게 특징입니다.

 

let taskState = ["start", "30% complete", "60% complete",
                 "90% complete", "finish"]

 

task의 state를 나타냅니다.

 

func async_vs_sync(){
        //1 DispatchQueue.global()
        callSyncTask()
        //2 DispatchQueue.main(serial)
        (0...4).map{ print("DEBUG: Second task's state: " + taskState[$0]) }
    }
    
    func callSyncTask() {
        let background = DispatchQueue.global()
        background.sync {
            (0...4).map{ print("DEBUG: First task's state: " + taskState[$0])}
        }
    }

 

 

1. task작업을 sync로 먼저! 호출합니다. 

2. callSyncTask() 함수 다음에 아래의 Second task를 호출합니다.

 

Thread가 callSyncTask()를 먼저 호출합니다. 코드의 실행순서는 위에서부터 실행되기 때문입니다. 그러나 main Thread는 주석 2를 실행할 수 없습니다. 언제까지? callSyncTask()에서 선언한 background.sync의 task가 끝나기 전까지 말입니다.

 

여기서 한 가지 궁금한 점이 있었습니다.

dispatchQueue's global queue는 디폴트로 concurrent입니다. dispatchQueue main의 mainThread와 dispatchQueue global의 특정 Thread는 같이, 동시에 실행될 수 있습니다. dispatchQueue global의 특징입니다. concurrent == dispatchQueue global의 여러 Thread와, dispatchQueue main의  main thread에서 thread 각각에 들어있는 task는 동시에 실행될 수 있다.

 

sync의 특징을 떠올리자면, sync안의 블록은 sync가 끝날 때까지 다른 로직을 수행할 수 없습니다. sync함수는 @escaping이 아닙니다. 

 

결론부터 말하자면 mainQueue는 GCD를 사용해 sync task를 submit 했을 때 main thread에서 실행될 수 있습니다.

 

dispatchQueue.global() 코드와 sync 블럭의 첫 실행은 dispatchQueue main의 Thread가 합니다. main queue는 dispatched 된 block이 완료될 때까지 기다려야 합니다. 메인 큐 이외의 큐에서 블럭을 처리할 때 main thread를 사용할 수 있습니다. sync는 주어진 queue인 global queue에서 task를 수행합니다. main thread나 main queue가 실행되는 것을 blocking 합니다. (sync의 block이 전부 끝날 때까지).

 

FIFO구조인 dispatchQueue main에서 dequeue 된 task는 global타입의 sync입니다. global queue에서 생성된 Thread에 의해 global thread에서 concurrent 하게 진행되야 합니다. concurrent queue의 특징은 task가 큐에 들어온 순서에 상관없이 시작합니다. 그러나 synchronous task operation은 서로 다른 스레드에 의해 처리될 수 있습니다. 그래서 serial queue처럼 작동됩니다.

 

정말 어렵습니다. 하지만 제 생각엔 이 말이 맞는 것 같습니다. Swift 단톡방에서도 같은 답이 나온 것 같습니다.

근데 공식문서를 찾아봤는데 메인 큐 이외의 큐에서 블럭을 처리할 때 main thread가 사용할 수 있다는 아직 찾지는 못했습니다.하지만 main Thread이외의 Thread는 task의 실행을 보장받지는 않는다고 나와있습니다.

 

func checkIsMainThread() {
    if Thread.isMainThread{
        print("task running in main thread")
    }else{
        print("task running in background thread")
    }
}

 

이 코드를 callSyncTask함수의 map 안에 추가한다면 쉽게 알 수 있습니다.

 

sync는 global queue에서 task를 수행하지만 main thread나 main queue를 blocking 하는 특징이 있습니다.( sync의 task가 끝날 때까지)결국 background.sync의 task가 전부 끝난 후 callSyncTask() 함수가 종료되야 비로소 dispatchQueue main의 freezing이 풀리고 dispatchQueue main의 mainThread는 Second task를 호출하며 실행할 수 있습니다. 결국엔 sync함수는 함수이고 함수가 종료된 이후에 클로저 부분은 호출될 수 없습니다(@escaping이 아니기 때문입니다.) Global queue에 enqueue 이후 Thread에서 해당 task. 수행 -> 다시 dispatch queue로 돌아와 다음 task수행..)

 

GCD를 통해 sync타입으로 코드를 작성했다면 main thread에서 task가 실행될 수 있습니다. queue에 의해 dequeue 된 블럭이 sync task라면 dispatchQueue main은 시작한 후 끝날 때까지 기다립니다. 결과적으로 main thread나 main queue를 차단합니다.(main queue에서 다른 거 실행 못하도록) dispatched된 (sync)block이 완료될 때 까지 main queue는 대기해야 합니다. main thread는 main queue 이외의 다른 queue에서 task block들을 수행할 수 있기 때문입니다. 

 

(제 생각) 살짝 이해가 가는 부분이라면 sync는 @escaping이 아니기 때문에 주어진 work가 global에서 수행된다고 해도 초기에 main queue의 영역에서 호출되고 globaclQueue에 submit 됩니다. main queue -> global queue(concurrent) 근데 결국 @escaping이 아니기에 sync함수가 global queue에서 완료될 경우 다시 main Thread 영역에서 sync함수가 종료되야 합니다. @escpaing이 아니기 때문에, 그래서 이와 같은 경우엔 위의 내용처럼 진행됩니다....

 

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

실질적으론 global thread에 의해 실행되야 하는 것이 맞지만 queue에 전달된 task가 main thread에서 수행되는 것을 제외하고, system은 특정 thread가 task를 수행하는데 보장하지 않는다고 나와있습니다. 

 

sync는 기다린다는 뜻이고 실질적으로 다른 thread에 보내는 의미가 없다고 합니다. 결국 이 코드에선 main thread에서 실행됩니다.

 

GCD's async func

https://developer.apple.com/documentation/dispatch/dispatchqueue/2016098-async/

GCD의 async함수입니다. group, QoS, flags는 나중에 다시 소개할 것이기에 execute만 보면 됩니다. 마찬가지로 () -> Void 타입인데 @escaping을 띄고 있습니다. @escaping이란 async() 함수가 종료된 후에 work의 () -> Void가 실행될 수 있다입니다. 클로저는 캡쳐의 특징이 있기 때문에 async함수가 메모리에서 해제되도 비 동기적으로 호출될 수 있습니다.

 

func async_vs_sync(){
        callSyncTask()
        callSyncTask2()
        (0...4).map{ print("DEBUG: Third task's state: " + taskState[$0]) }
    }
    
    func callAsyncTask() {
        let background = DispatchQueue.main
        background.async {
            (0...4).map{ print("DEBUG: First task's state: " + self.taskState[$0])}
        }
    }
    func callAsyncTask2() {
        let background = DispatchQueue.main
        background.async {
            (0...4).map{ print("DEBUG: Second task's state: " + self.taskState[$0])}
        }
    }

 

여기서 위의 코드와 다른 점은 sync가 아닌 async가 사용됬고 disptchQueue가 global이 아닌 main(serial)입니다. callSyncTask의 background 변수로  일단 DispatchQueue.main을 사용했으므로 task는 전부 DispatchQueue.main이 다루는 Thread1에 서 코드를 실행합니다.

callSync가 아니라 callAsyncTask()입니다!!  async로 GCD 사용했는데 함수명을 안바꿨습니다..

322라인의 함수 안 async task를 Swift는 호출했지만 실행을 안 했습니다.

323라인의 함수 안 async task를 호출했지만 실행을 안 했습니다. 

그리고 324라인의 포문이 실행됬습니다. -> 4번 반복하고 종료 async_vs_async() 함수를 종료했습니다.

 

DispatchQueue.main은 Thread1을 통해 각 라인의 코드 속 함수를 호출하거나 실행합니다. 근데 callSyncTask()를 호출했는데 그냥 지나가 버렸습니다. callSyncTask2()도 호출했는데 지나가 버렸습니다. 원래는 함수를 호출할 때, 안에 있는 코드를 전부 실행할 텐데 말입니다. callSyncTask 함수들의 task가 asynchronously 하기 때문입니다. async에 사용한 클로저는 @escaping이기 때문에 가능합니다.

 

callSyncTask() 호출될 때 안의 내용을 실행해야 하지만 "나중에 하겠다 == asynchronously" 그리고 즉시 반환합니다. 그 대신 DispatchQueue.main이 담당하는 Thread(Thread1)에서 언젠가 다시 수행할 것입니다. 수행되는 타이밍을 잘 모르겠는데. 정말 asynchronously 하기 때문에 수행될 타이밍을 모르는 게 맞는 거겠죠? '나중'이라는 말은 지금 수행할 수도 있고 나중에 수행할 수도 있습니다.

수행하다 다른 우선순위에 밀려 좀 있다 수행하는 경우가 있습니다. 근데 이 queue의 type이 serial이라 수행하다 다른 우선순위에 밀려 좀 있다 수행하지는 않습니다. 한번  task가 실행되면 반드시 끝날 때까지 기다립니다.(Thread는 한 개의 task만 실행할 수 있는 게 serial의 특징,,

call async task를 더 추가한 경우에도 serial. 이전 task가 끝나야 다음 task 작동

그렇다면 이번엔 callAsyncTask의 background dispatchQueue 타입을 main이 아니라 global로 바꾼다면?

func callAsyncTask() {
    let background = DispatchQueue.global()
    background.async {
        (0...4).map{
            print("DEBUG: First task's state: " +
                           self.taskState[$0])
            self.checkIsMainThread()
        }
    }
}

// callAsyncTask2()
// callAsyncTask3() 전부 background를 DispatchQueue.global()로 변경


//호출하는 함수

func async_vs_sync(){ // 호출 구간 : viewDidLoad()안에 이니다.
    callAsyncTask()
    callAsyncTask2()
    callSyncTask3()

    (0...4).map{ print("DEBUG: dispatchQueue.main task's state: " + self.taskState[$0])}
}

 

 

callAsyncTask()이 경우 함수 안에서 background의 DispatchQueue는 global이고 concurrent입니다. async 클로저는 @escaping이기 때문에 async함수가 종료되고 global queue's Thread에 추가됩니다.

// 반대로 sync를 사용한 경우엔 sync함수가 호출된 이상 종료되기 전까지 반드시 실행되야 함으로 global queue의 Thread로 옮겨가지 않고 mainThread가 계속해서 작업하는 것과 같은 결과를 얻는다고 해서 main thread에서 실행됩니다.

 

마찬가지로 callAsyncTask2(), callAsyncTask3() 또한 각각 globalQueue에 의해 Thread를 생성하고 각자가 동시에 실행됩니다.

 

결론

other queue's sync task

case 1: Task type is sync. Send other queue

dispatchQueue.main에서 다른 큐에 sync 작업을 추가하면 @escaping이 되지 않기에 다른 큐의 쓰레드에서 작업하는 동안 dispatchQueue는 sync함수를 호출한 상대여서 기다려야 합니다. 결국 dispatchQueue의 main thread는 놀고 있는데 dispatchQeuue는 main thread 이외의 thread의 실행을 보장하지 않습니다. 그래서 기다리고 있는 main thread가 수행을 하고 이는 serial적으로 보입니다. 

 

case 2: Task type is sync. Send dispatchQueue.main

만약 dispatchQueue.main 에서 dispatchQueue.main.sync로 작업을 추가하면 어떻게 될까요?

 

other queue's async task

case 1: Custom queue(serial)

dispatchQueue.main은 async함수를 호출하고 @escaping을 통해 종료합니다. 그리고 other queue는 자신의 Thread를 생성해 async함수를 호출합니다.

 

여기서 또 갈릴 수 있습니다.

내가 생성한 큐가 커스텀 큐이고 serial이다. "label: haha"라는 dispatchQueue에 다른 작업이 들어오지 않고

모두가 개별 큐를 선언했을 땐

async의 특성상 Thread1에서 수행된 async함수가 종료된 후에 각각 생성했던 고유의 Queue에 의한 Thread에서 개별적으로 serial 하게 수행됩니다.

여기서 Thread1은 main Thread를 의미합니다. Thread2, Thread4, Thread7은 backgound therad이기 때문에 mainThread가 아닌 custom background thread에 해당합니다. 각각의 Thread는 serial이기 때문에 한 개의 task만 실행할 수 있고 Thread2와 Thread4, Thread7은 모두 자신만의 일을 독립적으로 수행합니다.

중요한 것은 각각의 쓰레드는 자신만의 일을 수행했다는 점! 

 

case 2: Global(concurrent)

그렇다면 커스텀이 아닌 dispatchQueue.global()인 경우는 어떨까요? (위에서 봤지만,,)

gloabl은 concurrency입니다. 이전의 커스텀 큐는 serial이었지만 그럼에도 callAsyncTask1,2,3은 각각 고유한 global타입의 dispatch queue를 보유합니다.

역시 각각 고유한 Thread를 보유합니다. main thread와 같이 수행됩니다. 

 

sync 덕분에 많은 개념을 알 수 있었네요..

다음엔 GCD와 dispatchQueue에 관한 자세한 개념들을 탐구하려고 합니다. 

내용 중 틀린 부분이 있다면 댓글로 알려주신다면 정말 감사합니다.

 

 

참고자료

https://stackoverflow.com/questions/19179358/concurrent-vs-serial-queues-in-gcd/53582047#53582047

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

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW1

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1

https://www.kodeco.com/28540615-grand-central-dispatch-tutorial-for-swift-5-part-1-2