[Swift] GCD (Grand Central Dispatch)

SangOK·2022년 9월 15일
0

이론

목록 보기
2/2

소스 코드 확인하기 <-

Swift 는 동시성 프로그래밍을 위해 GCD (Grand Central Dispatch) 그리고 Operation API 를 제공합니다.

이 게시글에서는 GCD 에 대해서 다뤄보겠습니다.

Concurrency (동시성) 란?
앱의 로직 내에서 특정 부분이 동시에 또는 임의의 순서로 실행되는 것을 말합니다.
동시성 여러 작업을 수행할 수 있어 성능 향상에 이점을 가져다 줄 수 있습니다.
동시성은 데이터 흐름을 유지하는 것이 중요하기 때문에 동시에 이루어지는 두 작업에서 하나의 데이터를 조작하면 안 됩니다.

동시성은 성능 향상 뿐 아니라 유저의 사용자 경험 측면에서도 이점이 있습니다.
이미지를 다운받는 작업을 한다고 가정을 했을 때, 네트워크의 환경으로 인해 시간이 지연된다면 UI를 다루는 작업이 수행되지 않아 앱이 멈추게 되고 사용자에게 불편함을 주게 될 것입니다.
그러나 동시성으로 UI 작업과 이미지를 다운받는 작업을 동시에 수행하게 된다면 이 문제를 해결할 수 있습니다.


Process, Thread, Task

  • Process
    하나의 프로그램이 메모리에서 실행되는 작업 단위
    한 컴퓨터가 여러 개의 Process 를 갖게 되면 Multi Process
  • Thread
    하나의 Process 내에서 실행되는 작업의 단위
    Process 안에 적어도 하나 이상의 Thread 가 존재한다. 두개 이상의 Thread 를 갖게 되면 Multi Thread
  • Task
    스케줄링의 대상이 되는 작업

GCD
GCD 는 스레드 풀을 관리하며 Dispatch queue 에 있는 작업들을 사용 가능한 스레드에 스케쥴링 해줍니다. 개발자는 스레드 관리에 대해선 신경쓰지 않아도 되고 작업을 Dispatch queue 에 넣어주기만 하면됩니다. 해당 작업들은 시스템이 알아서 스레드에 할당합니다.

Serial vs Concurrent
DispatchQueue 에는 serial queue 와 concurrent queue 가 있습니다.
serial queue 는 오직 "하나"의 스레드에서만 수행하고 concurrent queue 는 "여러" 스레드에서 수행합니다.

Sync vs Async - 작업을 보내는 시점에서 기다릴지 말지에 대해 다루는 것
큐에 들어가있는 작업들은 동기적 또는 비동기적으로 수행될 수 있습니다.
동기적으로 수행될 경우 현재 run loop 가 끝날 때까지 기다립니다.
비동기적으로 수행될 경우 현재 작업을 수행하는 즉시 반환되고 다음 작업을 바로 수행합니다.

Serial vs Concurrent / Sync vs Async 차이점

  • Serial vs Concurrent
    Queue(대기열)로 보내진 작업들을 여러개의 스레드로 보낼 것인지 한개의 스레드로 보낼 것인지에 대해 다루는 것

  • Sync vs Async
    작업을 보내는 시점에서 기다릴지 말지에 대해 다루는 것

    sync 메소드에 대한 주의 사항 3가지

    1 . 메인 스레드 에서 다른 스레드로 (globalQueue) 보낼 때 sync 를 사용하지 말자

    why!? sync 로 보낸다는 뜻은 해당 작업이 끝날 때까지 기다린다 입니다. 그런데 UI 를 업데이트 해야하는 메인 스레드에서 다른 작업이 끝날 때까지 기다린다면 메인 스레드의 UI 업데이트가 지연된다는 의미이고 이는 화면이 멈출 수 있다는 뜻 입니다.
    (메인 스레드에서는 항상 async 로 작업을 보내도록 하자!)

    2 . 같은 큐에 sync 로 작업을 보내지 말자

    why!? Task A 작업이 실행 도중에 Task B 를 만나 큐에 보냅니다. GCD 는 Task B 적당한 스레드에 할당하겠지만 만약 Task A 작업이 실행중인 스레드에 할당된다면? 데드락 상태가 됩니다.
    ! 글로벌 큐는 Qos 에 따라 각각 다른 큐 객체를 생성하고 다른 Qos 큐를 사용하면 데드락 X

    //데드락 발생 가능성 있음
    DispatchQueue.global().async {
    DispatchQueue.global().sync
    }

    //데드락 발생 가능성 없음
    DispatchQueue.global(qos: .utility).async {
    DispatchQueue.global().sync
    }

    3 . 메인 스레드 DispatchQueue.main.sync 사용하지 말자

    why!?

    이러한 에러가 발생합니다!
    mainQueue 는 SerialQueue 이고 메인 스레드 (결국 동일 스레드 사용) 에만 할당합니다.
    거기에 sync 로 인해 보낸 작업이 끝날 때까지 기다린다? 결국 1번과 비슷하게 데드락 상태가 됩니다.


나오는 4가지 조합
  • SerialQueue.sync
    메인스레드의 작업이 queue 에 넘긴 작업이 끝날 때까지 기다리고 넘겨지는 작업들은 모두 같은 스레드로 보내지기 때문에 이 전 작업이 끝나야 실행된습니다.

  • SerialQueue.async
    메인스레드의 작업이 queue 에 작업을 넘기자 마자 반환되고, 넘겨지는 작업들은 모두 같은 스레드로 보내지기 때문에 이 전 작업이 끝나야 실행된습니다.

  • ConcurrentQueue.sync
    메인스레드의 작업이 queue 에 넘긴 작업이 끝날 때까지 기다리고 넘겨지는 작업들은 서로 다른 스레드로 보내질 수 있기 때문에 먼저 넘겨진 작업이 끝나지 않아도 실행될 수 있습니다.

  • ConcurrentQueue.async
    메인스레드의 작업이 queue 에 작업을 넘기자 마자 반환되고, 넘겨지는 작업들은 서로 다른 스레드로 보내질 수 있기 때문에 먼저 넘겨진 작업이 끝나지 않아도 실행될 수 있습니다.

Dispatch Queue 의 종류

  • Main Queue
    단 한개만 존재하며 serial 특성을 가진 Queue
    이곳에 할당된 task 는 메인 스레드에서 처리 (UI 업데이트)

  • Global Queue
    한개 이상 존재하며 concurrent 특성을 가진 Queue
    QoS (Quality Of Service) 에 따라 6개개의 종류로 나뉨
    ( DispatchQueue.global(qos: .6가지) or .async(qos: .6가지))

    6가지
    userInteractive. 사용자와 직접 상호작용 하는 작업 (UI 작업, 애니메이션)
    userInitiated. 즉각적인 결과가 필요한 작업 (저장된 문서 열기)
    default. 일반적인 작업
    utility. progress bar와 함께 길게 실행 (데이터 다운)
    background. 유저가 직접 인지하지 않는 시간이 덜 중요한 작업 (동기화 및 백업)
    unspecified. QoS 정보가 없음 (거의 사용 X)
    우선 순위가 더 높은 일을 더 많은 스레드에 배치
    번외 -> DispatchQueue.global(qos: .background).async(qos: .utility)
    위와 같은 경우는 더 높은 우선순위를 따라가는 듯..!?

  • Custom Queue
    디폴트로 serial 특성을 가지고 concurrent 로 설정 가능
    3가지 방법으로 생성
    DispatchQueue(label: "조상횬")
    DispatchQueue(label: "조상횬", attributes: .concurrent)
    DispatchQueue(label: "조상횬", qos: .utility, attributes: .concurrent)

  • Dispatch 사용 주의사항
    객체 캡쳐 조심하기
    Task 를 큐에 보내면 결국 클로저를 사용하는 것 입니다.
    따라서 객체에 대한 캡쳐가 발생할 수 있고 이는 retain cycle 에 대해 생각해봐야 한다는 뜻 입니다.
    (다음 게시글로 retain cycler 작성하기)

DispatchQueue Group 사용법

분배 된 스레드들의 작업이 끝나는 시점을 한번에 파악하고 싶은 경우에 사용
(10 개의 이미지 다운로드 작업(그룹) 중 한두개가 완료 된 시점이 아니라 10개 모두 완료 된 시점)
1 . let myGroup = DispatchGroup()
2 . DispatchQueue.global().async(group: myGroup) { // run }

  • 다른 스레드로 할당되더라도 myGroup (같은 그룹) 으로 지정 됩니다.
    DispatchQueue.global(qos: .userInitiated).async(group: myGroup) { // run }
    DispatchQueue.global(qos: .background).async(group: myGroup) { // run }

  • notify(queue: .main) 그룹 내 Task 가 모두 완료했을 때
    myGroup.notigy(queue: main) { // UI Update }

  • wait(timeout: .now() + X)
    현재 그룹에 포함되어있는 작업들이 모두 끝날 때까지 현재 스레드를 멈추는 메소드 timeout 파라미터를 사용해 최대치를 정할 수 있고 이후 다음 작업이 진행된다.
    return DispatchTimeoutResult -> .success (성공), .timedOut (실패)

    주의사항
    1 . 메인 스레드에서 사용 X !! 다른 스레드 종료 시 까지 메인 스레드의 블락...
    2 . Task 가 현재 스레드에 할당되면 안됩니다. 데드락!

    사용법
    DispatchQueue.global(qos: .userInitiated).async(group: myGroup) { // run }
    DispatchQueue.global(qos: .userInitiated).async(group: myGroup) { // run }
    myGroup.wait()
    what!?
    Thread 2 실행중 2개의 Task 가 Thread 3, Thread 4 에 할당되었을 때 2개의 Task 가 종료될 때까지 Thread 2 가 대기한다.

    번외
    실행시킨 Task 1 내부에 async 즉 비동기 Task 2 를 실행시킨 다면 어떻게 될까?
    async 이기 때문에 Task 2가 다른 스레드에 할당 된다면 다시 Task 1 이 실행되고 여기서 Task 2가 끝나지 않았어도 Task 1이 끝난다면 해당 Task 는 끝난 것으로 인식합니다. 즉 그룹의 원하지 않은 타이밍에 종료를 알릴 수 있습니다.
    여기서 필요한게 enter() (+1), leave() (-1) 메소드 입니다.
    서로 짝으로 사용되며 task reference count 를 관리하고 count 가 0 이 되면 그룹 종료!

    테스트 코드

profile
궁금하다

0개의 댓글