봄날은 갔다. 이제 그 정신으로 공부하자

Dispatch Queue 본문

iOS Tip

Dispatch Queue

길재의 그 정신으로 공부하자 2022. 11. 29. 22:22

Dispatch Queue란?

GDC(Grand Central Dispatch) Dispatch Queue는 동기 or 비동기식으로 Task를 수행할 수 있는 강력핟 도구 입니다.

개발자는 Dispatch Queue를 사용하여 그동안 별도의 Thread를 통해 수행하던 거의 모든 Task를 수행할 수 있습니다.

Dispatch Queue의 장점은 사용하기 쉽고 Thread 코드보다 Task를 실행이 더 효울적이라는데 있습니다.

 

Dispatch Queue는 "큐"라는 이름에서 알 수 있듯이 FIFO(First-In, First-Out) 형태의 자료 구조를 가지므로 

Dispatch Queue에 추가된 Task는 항상 추가된 순서대로 시작되며 Serial과 Concurrent 두가지 타입을 가집니다.

 

Serial & Concurrent

Serial 

큐에 추가된 순서대로 한번에 하나의 Task만 순차적으로 실행됩니다.

큐에 3개의 Task가 추가되었더라도 추가된 순서대로 Task가 실행되고 작업 중인 Task가 끝난 후 다음 Task가 실행되는 방식이므로

여러개의 Task를 동시에 실행하기에는 부적합하지만 Serial Queue는 특정 자원에 대한 액세스를 동기화하는데 자주 사용됩니다.

 

Concurrent

Task를 큐에 추가된 순서대로 실행합니다. Serial과는 다른 점은 Serial은 실행한 Task가 끝날때까지 기다렸다 끝난후 다음 Task를 실행하는 반면 Concurrent는 실행한 Task의 종료와 상관 없이 병렬적으로 그냥 다음 Task를 실행합니다.

 

두가지 방식의 차이점을 그림으로 설명하면 다음과 같습니다.

 

Main & Global

GCD는 Serial(순차적)하게 동작하는 Main Queue와 Concurrent(병렬적)하게 동작하는 Global Queue를 제공합니다.

일반적인 비동기 처리는 빠른 작업 수행을 위해 Global Queue를 사용해 병렬적으로 동시에 수행하면 되지만 비동기 처리 중 UI 구성요소에 대한 접근이 필요할 경우에 Main Queue를 사용해 처리하여야 합니다.

 

Global Queue는 병렬적으로 동시에 실행할 수 있지만 개발자가 QoS를 통해 우선순위를 결정해 줄 수 있습니다.

1. userInteractive

   애니메이션, 이벤트 처리 또는 앱의 UI업데이트와 같은 user-interactive작업에 대한 QoS로 우선순위가 가장 높습니다.

2. userInitiated

   QoS는 유저가 무언가를 클릭했을 때 곧장 일을 처리하도록 하는 옵션으로 두번째 우선 순위를 가집니다.

3. default

   user-interactive나 user-initiated보다 우선순위가 낮지만 유틸리티나 백그라운드 작업보단 우선순위가 높습니다. 

   앱이 시작할 때 혹은 사용자를 대신하여 active work을 수행하는데 사용하는 큐나 작업에 이 클래스를 할당합니다.

4. utility

   사용자가 앱을 계속 사용할 수 있도록 차단하지 않는 작업에 대해 이 QoS클래스를 할당합니다.

5. background

   앱이 백그라운드 상태에 있는동안 수행하는데 사용되는 작업이나 DispatchQueue들에게 이 클래스를 할당합니다.

6. unspecified

   QoS정보가 없음을 시스템에 신호를 줍니다.

 

사용 방법은 다음과 같습니다.

주의해야 할 부분은 QoS를 통해 실행 우선 순위를 보장받는다고 해도 그것이 절대적인 것은 아니라는 것 입니다.

DispatchQueue.global(qos: .userInteractive).async {
    for item in 1 ... 10 {
        print("Global Queue (userInteractive): \(item)")
    }
}

DispatchQueue.global(qos: .default).async {
    for item in 11 ... 20 {
        print("Global Queue (default): \(item)")
    }
}

DispatchQueue.global(qos: .background).async {
    for item in 21 ... 30 {
        print("Global Queue (background): \(item)")
    }
}

 

sync & async 

앞서 설명한 Dispatch Queue는 동기 or 비동기 방식으로 실행 할 수 있습니다.

위 예제코드는 async(비동기)로 구현되어 있는데 이를 sync(동기)방식으로 아래와 같이 변경하게 되면 

DispatchQueue.global(qos: .default).sync {
    for item in 11 ... 20 {
        print("Global Queue (default): \(item)")
    }
}

DispatchQueue.global(qos: .userInteractive).sync {
    for item in 1 ... 10 {
        print("Global Queue (userInteractive): \(item)")
    }
}


DispatchQueue.global(qos: .background).sync {
    for item in 21 ... 30 {
        print("Global Queue (background): \(item)")
    }
}

 

QoS는 무시되고 DispatchQueue에 입력된 순서대로 동작하게 됩니다.

출력 결과는 아래와 같습니다.

Global Queue (default): 11
Global Queue (default): 12
Global Queue (default): 13
Global Queue (default): 14
Global Queue (default): 15
Global Queue (default): 16
Global Queue (default): 17
Global Queue (default): 18
Global Queue (default): 19
Global Queue (default): 20
Global Queue (userInteractive): 1
Global Queue (userInteractive): 2
Global Queue (userInteractive): 3
Global Queue (userInteractive): 4
Global Queue (userInteractive): 5
Global Queue (userInteractive): 6
Global Queue (userInteractive): 7
Global Queue (userInteractive): 8
Global Queue (userInteractive): 9
Global Queue (userInteractive): 10
Global Queue (background): 21
Global Queue (background): 22
Global Queue (background): 23
Global Queue (background): 24
Global Queue (background): 25
Global Queue (background): 26
Global Queue (background): 27
Global Queue (background): 28
Global Queue (background): 29
Global Queue (background): 30

 

Dispatch Group

GCD는 Dispatch Queue외에도 코드를 관리하는데 도움이 되는 몇가지 기술을 제공합니다.

여기서는 Dispatch Group에 대해서만 설명 합니다.

 

Dispatch Group는 완료를 위한 블록 객체 집합을 모니터링하는 방법입니다.

개발을 하다보면 다수개의 Task를 동시에 실행하고 모든 Task가 완료되었을 때 다음 Task를 처리하는 경우가 많은데 Dispatch Group을 사용하며 이를 효율적으로 동기화하여 사용할 수 있습니다.

 

Dispatch Group의 핵심 개념은 enter(Group에 task +1), leave(Group에 task -1), notify(Group의 task가 0이 되었을 때 실행) 3개로 Dispatch Queue를 async로 실행할 때 group을 추가하면 enter()가 자동으로 호출되고 Dispatch Queue가 끝날 때 leave()가 자동으로 호출되어 모든 Dispatch Queue가 완료되면 자동으로 하단의 notify()가 호출됩니다.

사용 방법은 아래와 같습니다.

// 작업을 실행할 Queue 2개
let queue1 = DispatchQueue(label: "queue1", qos: .default, attributes: .concurrent)
let queue2 = DispatchQueue(label: "queue2", qos: .default, attributes: .concurrent)
// 작업을 하나로 묶을 그룹
let group = DispatchGroup()
        
// queue 실행 시 group으로 묶음
queue1.async(group: group) {
    for item in 1 ... 10 {
        print("Queue1: \(item)")
    }
}
        
// queue 실행 시 group으로 묶음
queue2.async(group: group) {
    for item in 11 ... 20 {
        print("Queue2: \(item)")
    }
}
        
// 작업 완료 시 결과를 받을 queue
let completeQueue = DispatchQueue(label: "completeQueue", qos: .default, attributes: .concurrent)
group.notify(queue: completeQueue) {
    print("End of All Task")
}

 

실행 결과는 다음과 같습니다.

Queue1: 1
Queue1: 2
Queue1: 3
Queue1: 4
Queue1: 5
Queue2: 11
Queue1: 6
Queue2: 12
Queue1: 7
Queue1: 8
Queue1: 9
Queue1: 10
Queue2: 13
Queue2: 14
Queue2: 15
Queue2: 16
Queue2: 17
Queue2: 18
Queue2: 19
Queue2: 20
End of All Task

 

Tip

만약 다중으로 네트워크 API 호출 후 결과를 비동기적으로 받고 모든 결과를 받은 후 처리하고자 할 때는 위 코드가 정상적으로 동작하지 않을 수 있습니다.

그 이유는 네트워크 호출 결과를 비동기적으로 받기 때문에 Dispatch Queue의 Task가 네트워크 호출 후 완료 처리되기 때문으로 그럴때는 아래와 같이 Dispatch Queue 사용 없이 Dispatch Group만 사용해도 됩니다.

 

let group = DispatchGroup()
        
group.enter() // 여기서 task +1
MyNetworkModel.getList(offset: 0, limit: 20){ result in
    // 작업할 내용
    group.leave() // 여기서 task -1
}
        
group.enter() // 여기서 task +1
MyNetworkModel.getInfo(myInfo: userId){ result in
    // 작업할 내용
    group.leave() // 여기서 task -1
}

// 작업 완료 시 결과를 받을 queue
let completeQueue = DispatchQueue(label: "completeQueue", qos: .default, attributes: .concurrent)
group.notify(queue: completeQueue) {
    print("End of All Task")
}

 

Comments