[iOS] DispatchQueue

Emily·2024년 12월 24일

DispatchQueue란, Swift에서 동시성 프로그래밍을 돕기 위해 만든 GCD(Grand Central Dispatch) 프레임워크 기술이다.

동시성 프로그래밍은 뭐고, GCD는 뭐고, 그래서 결론적으로 DispatchQueue는 뭘까 용어들을 차근차근 정리해보려 한다.

동시성 프로그래밍

동시성 프로그래밍이란, 여러가지 일을 동시에 작업하도록 하는 프로그래밍을 말한다. 동시성 프로그래밍을 가능하게 하는 건 멀티 스레딩이다. 스레딩이 뭘까? 여러 개의 스레드를 통해 비동기적으로 작업을 수행하는 것이다. 스레드는 뭘까? 프로세스 내에서 작업을 수행하는 단위다.

프로세스

실행 중인 프로그램, 실행하기 위해 메모리에 올라 온 프로그램을 프로세스라고 한다. iOS 앱도 프로세스라고 할 수 있다. 개발의 결과물인 은 iOS 운영체제에 속한 프로그램이다. 프로그램을 실행하면 프로세스라고 부르는 것이다. 프로세스 내부에는 여러 개의 스레드가 존재한다.

스레드

프로세스 내에서 작업을 수행하는 단위다. 한 개의 프로세스 내에서 여러 개의 스레드가 동시에 작업을 수행할 수 있다. 앱을 실행 시키면 여러 개의 스레드가 동시에 일을 하는 것이다.

크게 메인 스레드백그라운드 스레드로 구분한다. 여러 개의 스레드를 가지고 동시에 작업을 하는 것을 멀티 스레딩이라고 한다.

Grand Central Dispatch

GCD(Grand Central Dispatch)는 동시성 프로그래밍을 돕는 프레임워크다. Swift에서 사용하는 GCD 기술로는 DispatchQueue가 있다. 멀티 스레딩 작업을 하며 비동기적으로 작업을 수행하기 위해서 GCD 기술을 사용한다. 구체적으로 어떤 작업을 어떤 스레드에서 수행할지 코드로 지정하는 것이다.

DispatchQueue

DispatchQueue라는 단어의 의미를 살펴보면, 큐(Queue)를 어디론가 보낸다(Dispatch)는 말이다. 자료구조인 큐 중에서도 우선순위 큐는 데이터가 우선순위대로 빠져나가는 구조다. 우선순위는 내부 규칙에 따라 지정한다. 디스패치 큐가 이런 구조인데, 개발자가 작업을 디스패치 큐에 보내면 운영체제가 내부 규칙에 따라 적절한 스레드에 작업을 할당시킨다. (과거 프로그래밍에서는 개발자가 직접 스레드를 생성한 뒤 작업을 할당시켰다고 한다.)

DispatchQueue에 작업을 넘기는 코드는 아래와 같은 형식이다.

DispatchQueue.{Queue종류}.{QoS옵션}.{sync/async} {
	// 수행할 작업 코드 //
}

Queue 종류 : Main, Global, Custom
QoS(Quality of Service) : 작업 중요도
sync : 동기적으로 작업 수행
async : 비동기적으로 작업 수행

MainQueue

메인 큐는 앱의 메인 스레드에서 실행되는 큐로서 우선순위가 높은 UI 업데이트 관련 작업을 처리한다. 메인 큐의 종류는 직렬 큐(Serial Queue)다. 직렬 큐는 들어온 작업들을 한가지 스레드에 모두 보내고 작업을 한번에 하나씩 순서대로 실행하는 큐다. 스레드 하나에 모든 작업을 할당하기 때문에 작업 완료 순서가 보장된다.

DispatchQueue.main.async {
	// ui update code
}

메인 스레드에서는 sync 사용 시 데드락 현상이 발생하기 때문에 async만 사용해야 한다.

GlobalQueue

글로벌 큐는 백그라운드에서 실행되는 큐로서 사용자에게 당장 보일 필요가 없는 우선순위가 낮은 작업을 처리한다. 글로벌 큐의 종류는 동시성 큐(Concurrent Queue)다. 동시성 큐는 들어온 작업들을 여러 스레드에 나눠 보내고 동시에 병렬적으로 실행하는 큐다. 여러 스레드에서 동시에 작업하기 때문에 작업 완료 순서는 보장되지 않는다.

DispatchQueue.global().async {
	// network 통신, 계산이 무거운 작업 등을 백그라운드에서 수행
    
    // 결과를 ui에 반영할 때는 메인 스레드에서 수행
    DispatchQueue.main.async {
    	// ui update code
    }
}

Custom Queue

커스텀 큐는 개발자가 label에 큐의 고유한 이름을 설정할 수 있다. 또, 작업을 직렬 뷰에 보낼지 동시성 큐에 보낼지도 attributes에 지정한다. 지정하지 않으면 기본적으로 serial(직렬)큐로 설정된다.

// 커스텀 큐 정의
let customQueue = DispatchQueue(label: "com.myapp.customqueue", attributes: .concurrent)

// 커스텀 큐 실행
customQueue.asyns {
	// 커스텀 큐에서 실행할 작업 code
}

DispatchQoS

작업의 중요도를 시스템에 알려주는 값이다. 커스텀 큐를 생성할 때 우선순위를 부여하기 위해 사용한다. labelattributes처럼 qos 파라미터를 통해 부여한다.

let customQueue = DispatchQueue(
	label: "com.myapp.customqueue", 
	qos: .userInitiated,
    attributes: .concurrent
)

qos를 통해 OS에 작업의 중요도를 알려서 OS가 작업을 할당하는데 도움을 준다. 이를 통해 시스템은 리소스(CPU 시간, 에너지 등)를 효율적으로 분배할 수 있다.

우선순위가 높은순으로 살펴본다.

  • .userInteractive : 최고 우선순위. 지금 당장 해야하는 작업. UI 업데이트, 애니메이션 등 매우 빠르게 처리되어야 하는 작업을 할당한다.
  • .userInitiated : 높은 우선순위. 사용자가 기다리고 있는 작업. 버튼을 탭해서 데이터 로딩이 되는 상황 등 몇 초 안에 완료되어야 하는 작업을 할당한다.
  • .default : 기본 우선순위. 평범한 작업, 일반적인 백그라운드 작업을 할당한다. 특별한 지정이 없을 때 사용된다.
  • .utility : 낮은 우선순위. 천천히 해도 되는 작업. 데이터 다운로드나 백업 등 시간이 걸리지만 즉시 필요하지 않는 작업을 할당한다.
  • .background : 최저 우선순위. 언제 끝나도 상관없는 작업. 대용량 데이터 정리, 동기화 등 사용자가 직접적으로 인지하지 못하는 작업을 할당한다.
  • .unspecified : 시스템에게 우선순위 결정을 맡기는 값. 시스테이 알아서 우선수위를 결정한다. 어떤 우선순위로 처리해도 상관없는 작업을 할당한다.
Main Queue는 항상 .userInteractive로 처리된다.

실습

우선 작업이 완료되는 데에 n초가 걸리는 n번 task를 함수로 정의했다.

func testTask(_ taskNumber: Int, _ timeInterval: TimeInterval) {
    print("Task \(taskNumber) started")
        
    Thread.sleep(forTimeInterval: timeInterval)
        
    print("Task \(taskNumber) finished")
}
started를 출력한 n초 후에 finished를 출력하는 task

직렬 큐(Serial Queue)에서 sync/async 이해

// 직렬 큐 정의
let serialQueue = DispatchQueue(label: "com.myapp.myqueue")
serialQueue.sync {
	testTask(1, 2)
}
print("hello 1")
        
serialQueue.async { [weak self] in
	self?.testTask(2, 2)
}
print("hello 2")
        
serialQueue.async { [weak self] in
	self?.testTask(3, 2)
}
print("hello 3")

작업 수행에 2초가 걸리는 task 3개를 선언했다. task 1은 sync, task 2, 3은 async다. 각 task 사이에 큐에 할당하지 않은 일반 print문을 넣었다.

sync에 의해 Task 1이 완료된 뒤에서야 그 다음 print문이 실행된 모습이다. 또, 직렬 큐기 때문에 Task 2가 완료된 뒤 Task 3이 실행되었다. Task 2Task 3async기 때문에 그 사이에 있는 print이 먼저 출력되었다.

동시성 큐(Concurrent Queue)에서 sync/async 이해

// 동시성 큐 정의
let concurrentQueue = DispatchQueue(label: "com.myapp.myqueue", attributes: .concurrent)
concurrentQueue.sync {
	testTask(1, 2)
}
print("hello 1")
        
concurrentQueue.async { [weak self] in
	self?.testTask(2, 2)
}
print("hello 2")
        
concurrentQueue.async { [weak self] in
	self?.testTask(3, 2)
}
print("hello 3")

직렬 큐와 같은 task를 그대로 선언하였다.

sync에 의해 Task 1이 끝난 뒤에야 print문이 실행되는 것은 같지만, 동시성 큐에서는 Task 2Task 3이 한꺼번에 실행되는 것을 확인할 수 있다.

동시성 큐에서 QoS 이해

// Task 1 : background
concurrentQueue.async(qos: .background) { [weak self] in
	self?.testTask(1, 2)
}
        
// Task 2 : user interactive
concurrentQueue.async(qos: .userInteractive) { [weak self] in
	self?.testTask(2, 2)
}

// Task 3 : utility
concurrentQueue.async(qos: .utility) { [weak self] in
	self?.testTask(3, 2)
}

// Task 4 : user initiated
concurrentQueue.async(qos: .userInitiated) { [weak self] in
self?.testTask(4, 2)
}

Concurrent Queueasync로 각각 다른 QoS를 지정하여 4개의 똑같은 task를 선언하였다.

User InteractiveUser InitiatedUtilityBackground 순으로 출력된 것을 확인할 수 있다.

profile
iOS Junior Developer

0개의 댓글