지난 포스팅에서 "iOS는 GCD, NSOperation를 통해 멀티쓰레딩을 통한 동시성 프로그래밍을 지원한다" 언급하였는데, 이 중 GCD에 대해 이번 포스팅에서 알아보자.

 

우선 GCD는 애플에서 제공하는 "멀티 쓰레딩" 처리를 쉽고 편하게 해주는 API이다. 

또한 "큐에 넣기만 하면 알아서 쓰레드로 분배해 준다"라고 하였는데, 

GCD에서의 해당 큐가 Dispatch Queue이다.

 

 

Dispatch Queue

DispatchQueue는 아래와 같이 사용이 가능한데, "큐의 종류", "qos 옵션", "동기/비동기" 에 대한 설정이 가능한데, 이들에 대해 순차적으로 알아보자!

 

 

Queue의 종류 

Dispatch Queue의 종류에는 3가지가 있다. 

  • main Queue
  • global Queue
  • custom Queue

 

main Queue

main Queue작업들을 Main Thread에서 처리하며, UIApplicationMain은 자동으로 Main Queue를 생성하고 이를 Main Thread와 연결한다.  

Main Thread는 하나만 존재하며, Main Queue 역시 시스템에 의해 하나만 생성되기 때문에,

Main Queue를 불러올 때는 인스턴스를 호출하는 방식으로 불러온다.

public class var main: DispatchQueue { get }

또한 Main Thread 하나에서만 작동하기 때문에, Serial Queue이다.

 

for i in 1...5 {
    DispatchQueue.main.async {
        someTask(i)
    }
}

/* prints:
 Task1 Start
 Task1 Finish
 Task2 Start
 Task2 Finish
 Task3 Start
 Task3 Finish
 Task4 Start
 Task4 Finish
 Task5 Start
 Task5 Finish
*/

Serial Queue이기 때문에, 순차적으로 실행된 모습을 볼 수 있다.

 

global Queue

global Queue는 작업들을 Main Thread가 아닌 다른 Thread에서 처리하며, concurrent Queue이다.

global function의 파라미터로 전달되는 6가지 Qos 옵션에 따른 DispatchQueue의 인스턴스를 리턴한다.

 

public class func global(qos: DispatchQoS.QoSClass = .default) -> DispatchQueue

 

Qos 

System resources는 유한하기 때문에, 중요한 작업들의 경우에는 빠르게 처리할 수 있어야 한다. 

예를 들어, UI update는 background에서 실행되는 작업들보다 우선시되어야 한다. 

 

6가지 Qos(quality of service)를 통해, 이러한 우선순위를 개발자가 직접 지정해 줄 수 있는데,

우선순위가 높은 작업은 우선순위가 낮은 작업들보다 resources를 더 많이 사용하여, 빠르게 처리한다.

  • userInteractive : 이벤트 핸들링 혹은 애니메이션과 같은 UI update에 사용될 수 있다.
  • userInitiated : 사용자의 initiated 한 뒤로 처리가 이루어져야 하는 경우 사용되며, 파일을 열거나 버튼 클릭 후 액션을 수행하는 빠른 결과를 요구할 때 사용된다.
  • default 
  • utility : 네트워킹, 다운로드, I/O와 같이 길게 수행되는 작업들에 사용할 수 있다.
  • background : 동기화 및 백업과 같이 사용자 입장에서 오랜 시간이 소요돼도 상관없는 작업들에 사용할 수 있다.
  • unspecified : Qos정보가 없음을 나타내며, 시스템이 Qos를 추론해야 한다.

아래로 갈수록 우선순위가 낮아지기에, 작업을 처리하기까지의 소요시간이 더 길어진다.

for i in 1...5 {
    DispatchQueue.global().async {
        someTask(i)
    }
}

/* prints:
 Task3 Start
 Task2 Start
 Task4 Start
 Task1 Start
 Task5 Start
 Task2 Finish
 Task3 Finish
 Task5 Finish
 Task4 Finish
 Task1 Finish
*/

실제 결과가 Serial과 다르게 순차적으로 실행되지 않는다.

또한 Qos를 지정해주지 않으면, Qos가 default로 설정된다.

 

 

추가적으로, 아래와 같이 Queue만이 아닌 작업(Task)에도 Qos를 지정해 줄 수 있다.

let concurrentQueue = DispatchQueue.global(qos: .background)

concurrentQueue.async(qos: .background) {
    ...
}

하지만, Queue와 작업의 Qos가 서로 일치하지 않는 경우 작동 방식에 차이가 있다. 

 

우선, 작업의 Qos가 Queue의 Qos보다 큰 경우 (작업 Qos > Queue Qos)

해당 작업이 Queue에 존재하는 동안은 Queue의 Qos는 작업의 Qos로 상승하게 된다.

 

반대의 경우에는, (작업 Qos < Queue Qos)

작업의 Qos가 Queue에 Qos로 설정이 된다.

 

Custom Queue

시스템에 의해 생성된 Queue를 사용하는 것이 아닌, 유저가 직접 DispatchQueue를 생성할 수 있는데,

아래와 같이 생성 가능하다.

let customQueue = DispatchQueue(label: "custom")

customQueue.async {
    someTask(1)
}

이렇게 생성된 DispatchQueue는 Serial Queue가 되며, 

label의 경우에는 디버깅 툴에서 queue를 고유하게 식별하기 위함이다. 

 

Conccurent Queue는 다음과 같이 생성이 가능하다. 

let customQueue2 = DispatchQueue(label: "custom2", qos: .utility, attributes: .concurrent)

Qos의 경우에는 unspecified로 디폴트 파라미터로 지정되어 있기 때문에, 생략이 가능하다.

 

 

주의 사항 

UI는 반드시 Main Thread에서 처리

UI 업데이트를 다른 Thread에서 처리하게 되면, 런타임 에러가 발생한다.  

Multi-Threading으로 UI를 처리하게 되면, 

1번 Thread에선 Tableview의 특정 cell을 제거하고, 2번 Thread에선 해당 cell에 접근을 하게 되면 crash가 발생할 것이다.

이외에도 여러 가지 issue들이 존재하는데, 이를 해결하는 가장 쉬운 방법은 하나의 Serial Queue를 통해 처리하는 것이다. 

 

더욱 자세한 설명을 보고 싶다면 해당 링크를 읽어보길 바란다.

 

추가적으로, Main Thread에서 네트워킹과 같은 시간이 오래 걸리는 작업을 실행하면, 

즉, Main Thread가 오랫동안 응답하지 않으면, 0x8badf00d exception이 발생할 수 있다.

 

Main Thread에서 다른 Thread로 작업을 보낼 때,  항상 async로 보내야 한다. 

sync는 "해당 작업이 끝날 때까지 다음 작업을 실행하지 않고 기다리기" 때문에,

해당 작업이 끝날 때까지, Main Thread다른 작업을 실행하기 못하고 대기하는 상태에 빠진다.

이는, Main Thread에서 UI Update가 진행되지 않기 때문에, 화면이 버벅거려 보일 것이다.

 

현재와 같은 Queue로 작업을 보낼 때는,  async로 보내야 한다.

DispatchQueue는 생성하게 되면서, 특정 Thread와 연결이 되게 되는데, 

 

다음 코드를 살펴보자.

DispatchQueue.global().async {
    
    DispatchQueue.global().sync {
        task()
    }
}

//Global Queue는 Qos가 다르면, 다른 Queue이다.

우선 Main Thread에서 Default global Dispatch Queueasync 작업을 추가한다.

 

이전에 언급했듯이, 클로져 내부가 작업의 단위이기 때문에, Task1을 Queue에 추가하고 바로 리턴이 된다.(Main Thread로 돌아옴)

Queue는 Task1을 특정 Thread에 할당하여, 해당 Thread에서 수행하게 된다.

 

해당 Task1은 다음과 같다.

즉 Task1은 Default global Dispatch Queue에 sync로 task()를 추가하는 작업이다.

sync작업이기 때문에, task()가 완료되기까지 Task1이 수행되던 Thread는 기다리게 된다.

 

이때, 해당 Queue가 Task1과 같은 Thread에 작업을 할당하게 된다면, 

해당 Thread는 이미 기다리는 상태이기 때문에, task()를 수행하지 못하게 되는 현상이 발생한다.

즉, 두개 이상의 작업이 서로의 작업이 끝나기만을 기다리는 상황Dead Lock(교착상태)에 빠지게 된다.

 

 

Reference

Prioritize Work with Quality of Service Classes

Concurrency and Application Design

Dispatch Queues

DispatchQueue

iOS: Why the UI need to be updated on Main Thread

복사했습니다!