Swift

Swift - GCD (Grand Central Dispatch)

Goniii 2024. 12. 10. 15:05

GCD (Grand Central Dispatch)

  • Apple이 개발한 멀티스레드 프로그래밍을 쉽게 구현할 수 있도록 제공하는 프레임워크
  • 효율적인 동시성(concurrency) 처리를 지원하며 주로 비동기 작업 관리에 사용됨

 

GCD의 특징

  1. 저수준 스레드 관리
    • 스레드 생성 및 관리를 시스템이 자동으로 처리함
    • 개발자는 직접 스레드를 생성하거나 종료할 필요 없이 작업 큐에 태스크를 제출하면 됨
  2. 작업 큐 (Task Queue) 기반
    • 작업은 큐에 추가되며, 시스템은 적절한 스레드 풀을 사용하여 태스크를 실행함
    • FIFO(First In, First Out) 순서를 따르며, 우선순위에 따라 실행 순서를 조정함
  3. 성능 최적화
    • GCD는 CPU 코어 수에 따라 태스크를 효율적으로 분배함
    • 필요한 만큼의 스레드만 생성하여 리소스 낭비를 방지
  4. 비동기/동기 실행
    • 작업은 비동기적, 동기적으로 실행 가능

 

 

GCD의 핵심 개념

1. 디스패치 큐 (Dispatch Queue)

  • 작업을 순차적 또는 동시에 실행할 수 있는 큐
  • Serial Queue (직렬 큐)
    • 한 번에 하나의 작업만 처리
    • FIFO 순서로 실행됨
  • Concurrent Queue (동시 큐)
    • 여러 작업을 동시에 처리함
    • 작업 시작은 FIFO 순서에 따라 이루어지지만, 완료 순서는 보장되지 않음
  • Main Queue
    • 메인 스레드는 단 하나만 존재
    • Serial 특성을 가짐
    • UI 업데이트 담당
  • Global Queue
    • 시스템이 제공하는 기본 Concurrent Queue
    • QoS(Quality of Service)에 따라 작업 우선순위를 지정 가능함
  • Custom Queue
    • 개발자가 직접 생성하는 큐
    • Serial 또는 Concurrent로 설정 가능

 

2. 동기와 비동기(Synchronous vs Asynchronous)

특성 동기(Sync) 비동기(Async)

특성 동기(Sync) 비동기(Async)
실행 방식 호출한 작업이 완료될 때까지 기다림 작업을 바로 큐에 전달하고, 호출자는 즉시 반환
스레드 블로킹 호출 스레드(blocking) 발생 호출 스레드(blocking) 없음
용도 순차적인 작업 처리에 사용 작업 병렬 처리, 비동기 작업 처리에 사용

 

 

GCD의 주요 함수

1. Global Queue 

  • QoS(Quality of Service) 설정
DispatchQueue.global(qos: .background).async {
    print("Running in the background queue.")
}

 

2. Main Queue 

  • UI 업데이트는 반드시 main 큐에서 실행해야 함
DispatchQueue.main.async {
    print("Updating UI on the main queue.")
}

 

3. Dispatch Group

  • 여러 작업을 그룹화하여 모든 작업이 완료된 시점에 특정 코드를 실행할 수 있도록 함
let group = DispatchGroup()

DispatchQueue.global().async(group: group) {
    print("Task 1")
}

DispatchQueue.global().async(group: group) {
    print("Task 2")
}

group.notify(queue: .main) {	// 같은 그룹 내의 작업이 모두 완료되면 호출됨
    print("All tasks are completed.")
}

 

4. Serial Queue 

  • FIFO 순서로 순서가 언제나 보장됨 → 들어온 순서대로 작업
var numArr = [1, 2, 3, 4, 5]

let serialQueue = DispatchQueue(label: "serial") // default 값은 serial
numArr.map { num in
    serialQueue.async {
        print(num) // 순서가 언제나 같음
    }
}

/*
 1
 2
 3
 4
 5
 */

 

5. Concurrent Queue 사용

  • 여러 작업을 동시에 처리하기 때문에 완료 순서가 보장되지 않음
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)

numArr.map { num in
    concurrentQueue.async {
        print(num) // 순서가 보장되지 않고 작업이 끝나는대로 출력
    }
}
/*
 1
 2
 4
 3
 5
 */

 

 

GCD 주의사항!

1. 메인 스레드에서의 main.sync → 데드락 발생

  • 메인 큐는 직렬 큐로 동작함
  • 동기 작업(sync)은 호출한 스레드를 블로킹하므로 메인 스레드에서 DispatchQueue.main.sync를 호출하면 데드락이 발생함
var numArr = [1, 2, 3, 4, 5]

numArr.map { num in
    DispatchQueue.main.sync { // error: Execution was interrupted
        print(num) 
    }
}
  • Main DispatchQueue는 Serial 특성을 가지는데 sync를 호출하면 메인 스레드는 그대로 진행을 멈추고 등록한 작업이 끝날 때까지 기다림
    • Serial 스레드는 호출 스레드를 블로킹함 → 메인 스레드를 블로킹
  • 동시에 큐에 등록된 작업(print(num))은 메인 스레드에 할당됨
  • 메인 스레드는 등록된 작업을 시작하지 못하고 작업이 끝나지 않는 데드락 상태에 빠짐

 

해결 방법

→ 메인 큐에서 동기 작업 호출을 피하고 비동기 작업(async)로 처리

DispatchQueue.main.async { ... }

 

 

2. 직렬 큐에서 sync → 데드락 발생

let serialQueue = DispatchQueue(label: "serial") // 직렬 큐
serialQueue.async {
    serialQueue.sync { 
        print("DeadLock")
    }
}
  1. serialQueue.async로 작업이 등록됨
  2. 작업이 실행 중일 때, 동일한 직렬 큐에서 sync를 호출하여 새로운 작업을 등록함
  3. 직렬 큐는 한 번에 하나의 작업만 실행하므로, 현재 실행 중인 작업이 완료 되어야 다음 작업이 실행됨
  4. 현재 작업은 sync 호출로 인해 대기 상태가 되므로 데드락이 발생함

→ 가능한 한 글로벌 큐에서 작업이 서로 의존하지 않도록 설계해야 함