[TIL]11.26

rbw·2022년 11월 26일
0

TIL

목록 보기
51/98

GCD

참조

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

위 스택오버플로우 답변 하나에 대해 정리한 글입니다


작업을 동시에 수행하려면, 해당 작업의 큐를 만들고 GCD에 전달하면 됩니다. 그러면 GCD는 관련된 모든 스레딩을 처리합니다. 따라서 우리가 실제로 하고 있는 일은 대기하는 것뿐입니다.

큐에는 직렬 및 동시의 두 가지 유형이 있지만, 모든 큐는 서로에 대해 동시입니다. 어떤 코드를 백그라운드에서 실행하고 싶다는 사실은 다른 스레드(일반적으로 메인스레드)와 동시에 실행하고 싶다는 의미입니다. 따라서 모든 디스패치 큐(직렬 또는 동시)는 다른 큐와 관련하여 동시에 작업을 실행합니다.

Serial Queue

Private Queue 라고도 합니다.

이는 특정 큐에 추가된 순서대로 처음부터 끝까지 작업을 한 번에 하나씩 실행하도록 보장합니다. 이것은 디스패치 큐에 대한 논의에서 직렬화를 보장하는 유일한 방법입니다. 하지만 별도의 Serial Queue 에서의 작업은 다른 Serial Queue와 동시에 실행될 수 있습니다. 왜냐하면, 모든 큐는 서로 상대적으로 동시에 발생하기 때문입니다. 모든 작업은 별개의 스레드에서 실행되지만, 모든 작업이 동일한 스레드에서 실행되는 것은 아닙니다.

// private(non-global)큐는 기본적으로 직렬,
// iOS에서는 바로 사용할 수 있는 직렬 큐가 없으므로 직접 만들어야함.
let serialQueue = DispatchQueue(label: "serial")

// 속성을 통해 동시로 만들 수 있습니다.
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])

이 시점에서 개인 큐에 다른 속성을 추가하지 않았다면, 애플에서는 하나의 글로벌(동시성이 보장된다) 큐를 사용하라고 권장합니다.

Concurrent Queue

Global Queue 라고도 합니다,.

이는 작업을 동시에 실행할 수 있습니다. 작업은 특정 큐에 추가된 순서대로 시작되도록 보장되지만 직렬 큐와 달리 이 큐는 두 번째 작업을 시작하기 전에 첫 번째 작업이 완료될 때까지 기다리지 않습니다. iOS에서는 즉시 사용이 가능한 4개의 동시 큐가 제공됩니다.

let concurrentQueue = DispatchQueuee.global(qos: .default)

Retain-Cycle Resistant: 디스패치 큐는 참조 횟수가 계산되는 객체이지만, 전역 큐이므로 유지 및 해제할 필요가 없으므로 유지 및 해제가 무시됩니다. 속성에 할당하지 않고도 전역 큐에 직접 접근이 가능합니다.

이유로는, 메모리구조에서 데이터 영역에 전역변수, static 변수가 저장되므로 힙 영역에서 계산을 하는 ARC를 신경쓸 필요는 없기 때문입니다.. 아마두?


다음으로, 큐를 디스패치하는 방법에 동기식과 비동기식을 알아보겠슴니당

Sync Dispatching

이는 큐가 디스패치된 스레드(호출 스레드)가 큐를 디스패치한 후 일시 중지하고 다시 시작하기 전에 해당 큐 블록의 작업 실행이 완료될 때까지 기다림을 의미합니다. 동기적으로 디스패치 하는 코드는 아래와 같습니다.

DispatchQueue.global(qos: .default).sync {
    // task ~
}

Async Dispatching

이는 호출 스레드가 큐를 디스패치한 후에도 계속 실행되며 해당 큐 블록의 작업이 실행을 마칠 때까지 기다리지 않음을 의미합니다. 코드는 다음과 같슴다

DispatchQueue.global(qos: .default).async {
    // task ~
}

이제 직렬로 작업을 실행하기 위해 직렬 큐을 사용해야 한다고 생각할 수 있지만 이는 정확하지 않습니다. 여러 작업을 직렬로 실행하기 위해서는 직렬 큐를 사용해야하지만 스스로 격리된 모든 작업은 직렬로 실행됩니다.

whichQueueShouldIUse.syncOrAsync {
    for i in 1...10 {
        print(i)
    }
    for i in 1...10 {
        print(i + 100)
    }
    for i in 1...10 {
        print(i + 1000)
    }
}

위 작업의 큐를 직렬 또는 동시로 구성하거나, 동기 또는 비동기로 디스패치하는 방법에 관계없이 이 작업은 항상 직렬로 실행됩니다. 세 번째 루프는 두 번째 루프 전에 실행되지 않고, 두 번째 루프는 첫 번째 루프 전에 실행되지 않습니다. 이는 디스패치를 사용하는 모든 큐에 해당합니다.

예시들로 알아보겠습니당

concurrentQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
103
3
104
4
105
5

출력은 예상대로 뒤죽박죽이지만 각 큐의 작업은 직렬로 실행했음을 알 수 있습니다. 이것은 동시성의 가장 기본적인 예입니다.

직렬 큐의 예시

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

이것이 직렬화의 가장 기본적인(그리고 유일하게 가능한) 예제입니다. 동일한 큐의 백그라운드에서(메인 스레드로) 하나씩 실행되는 두개의 작업입니다. 그러나 두 개의 개별 시리얼 큐를 만들면 두 개는 서로에 대해 동시이기 때문에 출력은 다시 뒤죽박죽이 됩니다.

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue2.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
3
103
4
104
5
105

위 두 개의 개별 큐는 다른 큐를 신경 쓰지 않기 때문에 동시에 실행이 됩니다. 하지만 좀 다른 예를 보겠습니다.

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 1000)
    }
}

1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005

위의 예상 결과는 concurrentQueue는 기다리지 않고 실행이 되어야 한다고 생각하는데 조금 이상하게 나왔습니다. 이유로는 GCD가 작업을 더 빨리 실행할 수 있을 만큼 concurrentQueue의 우선순위가 충분히 높지 않았기 때문에 이런 결과가 나왔다고 할 수 있습니다.

따라서 모든 것을 동일하게 유지하고 전역 큐의 우선순위(Qos, 단순히 큐의 우선순위 수준인 서비스 품질)를 변경 하면 출력은 예상대로 나옵니다.

// 위 동시 큐를 아래와 같이 설정하면 직렬 큐를 기다리지 않고 동시로 실행함
let concurrentQueue = DispatchQueue.global(qos: .userInteractive)

또 다른 직렬로 출력하는 방법은 두 가지를 동시에 유지하면서 디스패치 방법을 변경하는 것입니다.

concurrentQueue.sync {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

동기화 디스패칭은 진행하기 전에 큐의 작업이 완료될 때까지 호출 스레드가 대기한다는 의미일 뿐입니다. 여기서 주의할 점은 첫 번째 작업이 완료될 때 까지 호출 스레드가 정지된다는 것입니다. 메인스레드가 되어서는 안되겠져 ? 따라서 DispatchQueue.main.sync { }는 안되는겁니다 ㅎ,ㅎ

마지막으로 리소스에 관해서는, 큐에 작업을 부여하면 GCD는 내부 관리풀에서 사용 가능한 큐를 찾습니다. 하나의 QoS당 사용 가능한 큐는 64개입니다. 따라서 애플은 관리에 대한 권장 사항을 제공합니다. 다음과 같이 직렬 큐를 생성할 것을 권장합니다.

애플은 private concurrent 큐를 만드는 대신, 전역 동시 큐를 만드는걸 추천, 직렬 작업의 경우, 직렬 큐의 대상을 전역 동시 큐로 설정합니다. 그렇게 하면, 스레드를 생성하는 개별 큐의 수를 최소화하면서 큐의 직렬화된 동작을 유지할 수 있습니다.

let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [],
                                autoreleaseFrequency: .inherit, target: .global(qos:.default))
profile
hi there 👋

0개의 댓글