[UIKit] Concurrency: Dispatch Queue & QoS & Attributes

Junyoung Park·2022년 12월 26일
0

UIKit

목록 보기
134/142
post-thumbnail

Mastering Concurrency in iOS - Part 2 (Dispatch Queues, Quality of Service, Attributes)

Concurrency: Dispatch Queue & QoS & Attributes

Dispatch Queue

Main Queue

  • 시스템이 생성한 메인 큐
  • 순차적으로 태스크를 할당, 메인 스레드를 사용
  • 다른 큐에서의 메인 스레드 사용은 지양됨 → UIKit이 메인 스레드에 묶여 있는 까닭에 모든 UI 관련 동작은 메인 큐에서 진행되어야 함

Global Cuncurrent Queues

  • 시스템이 생성한 글로벌 큐
  • 동시적으로 태스크를 할당
  • 메인 스레드가 아닌 다른 스레드를 사용
  • QoS를 통해 우선순위 결정
DispatchQueue.main.async {
    let threadCondition = Thread.isMainThread ? "Execution in Main Thread" : "Execution in Global Thread"
    print("Main Queue: \(threadCondition)")
}

DispatchQueue.global(qos: .background).async {
    let threadCondition = Thread.isMainThread ? "Execution in Main Thread" : "Execution in Global Thread"
    print("Global Queue: \(threadCondition)")
}
  • 글로벌 큐에서의 모든 qos를 적용한다 할지라도 메인 스레드는 오로지 메인 큐에서만 사용 가능

Application Box

  • NSRunloop → UIKit → 이벤트 대기 → AppDelegate ... → GCD Queue → DB Query...
  • 유저 인터렉션 등 UI 관련 이벤트는 메인 스레드 주관이지만, 해당 데이터를 얻기 위한 DB 조회 등 모든 사건을 메인 스레드에서 처리할 경우 불필요한 리소스 낭비
  • 글로벌 큐에서 제공하는 멀티 스레딩을 통해 리소스의 효율적 사용 가능
  • 싱글 코어 HW: HW의 코어가 하나라면, 코어 당 한 타임 퀀덤에 할당할 수 있는 큐는 하나라는 뜻. 즉 어떤 큐를 실행해야 할지 결정할 필요가 있음
  • 글로벌 큐에서 실행되는 여러 가지 종류의 태스크는 종류에 따라 우선순위를 설정 가능하기 때문에 어떻게 리소스를 사용할 것인지 결정 가능

QoS

  • User Interactive: 애니메이션(메인 큐에서의 UI 핸들링과 별도로 애플이 도입한 유저 인터렉티브 Qos는 현재 논쟁적 이슈) - UI 업데이트와 관련 있는지 체크
  • User Initiated: 직접적인 결과(테이블뷰 스크롤을 통해 다음 데이터를 즉각적으로 얻어와야 하는 시점 등) - 부드러운 UX를 제공하는 데 있어 필요한 데이터인지 체크
  • Utility: 오랫 동안 유지되는 태스크 - 유저가 해당 진행도를 알고 있는지 체크
  • Background: 유저에게 보이지 않는 태스크 - 유저가 해당 태스크를 알고 있는지 체크
  • 유저 인터렉티브 - 유저 이닛 - 유틸리티 - 백그라운드 순서대로 우선순위 보장
  • Default: 유저 이닛과 유틸리티 사이
  • Unspecified: QoS 정보가 없을 때
DispatchQueue.global(qos: .background).async {
    for i in 100...200 {
        print(i)
    }
}

DispatchQueue.global(qos: .userInteractive).async {
    for i in 0...99 {
        print(i)
    }
}
  • QoS가 서로 다른 글로벌 큐 간의 실행 순서는 보장할 수 없지만 종료 시점은 예측 가능 → 백그라운드 글로벌 큐의 태스크가 유저 인터렉티브 큐에서 실행한 태스크보다 늦게 종료됨은 보장할 수 있음

Attributes

  • concurrent: 커스텀 큐를 설정할 때 동시성 여부를 체크 가능
  • initiallyInactive: 시작 시점에는 액티브하지 않은 상태로 설정 가능
  • target queue: 커스텀 큐가 실제로 화면 뒤에서 사용하는 큐. 특정 디스패치 큐의 우선순위는 그 큐의 타겟 큐로부터 상속받음. 타겟 큐를 특정하지 않는다면 디폴트로 '디폴트 우선순위 글로벌 큐'가 타겟 큐가 됨
let a = DispatchQueue(label: "A")
let b = DispatchQueue(label: "B", attributes: [.concurrent, .initiallyInactive])
b.setTarget(queue: a)

b.async {
    print("Testing Thread Activation")
}

b.activate()

a.async {
    for i in 0...5 {
        print(i)
    }
}

a.async {
    for i in 6...10 {
        print(i)
    }
}

b.async {
    for i in 11...15 {
        print(i)
    }
}

b.async {
    for i in 16...20 {
        print(i)
    }
}
  • .initiallyInactive를 어트리뷰트로 설정하지 않고 이후 b 스레드에 타겟을 설정한다면 크래쉬 → 액티브한 상태의 스레드의 타겟 큐는 변경 불가능하기 때문에 비활성화 상태에서 타겟 큐를 설정, 이후 해당 스레드를 활성화해야 함

Auto Release Frequency

  • inherit: 타겟 큐로부터 상속
  • workItem: 개별의 오토 릴리즈 풀
  • never: 개별 오토 릴리즈 풀을 설정하지 않음

Serial Queue Async

var value: Int = 20
let serialQueue = DispatchQueue(label: "com.queue.Serial")

func doAsyncTaskInSerialQueue() {
    for i in 1...3 {
        serialQueue.async {
            if Thread.isMainThread {
                print("task running in main thread")
            } else {
                print("task running in global thread")
            }
            guard
                let imageURL = URL(string: "https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg"),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print("\(i) finished downloading")
        }
    }
}

doAsyncTaskInSerialQueue()

serialQueue.async {
    for i in 0...3 {
        value = i
        print("\(value) in next block")
    }
}

print("last line in playground")

/*
 task running in global thread
 last line in playground
 1 finished downloading
 task running in global thread
 2 finished downloading
 task running in global thread
 3 finished downloading
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 */
  • 디스패치 큐는 기본적으로 순차적으로 잡을 실행하는 시리얼 큐
  • 시리얼 큐로 들어가는 태스크를 비동기적으로 실행한다 할지라도 들어간 '순서'대로 시리얼 큐가 실행

Serial Queue Sync

var value: Int = 20
let serialQueue = DispatchQueue(label: "com.queue.Serial")

func doSyncTaskInSerialQueue() {
    for i in 1...3 {
        serialQueue.sync {
            if Thread.isMainThread {
                print("task running in main thread")
            } else {
                print("task running in global thread")
            }
            guard
                let imageURL = URL(string: "https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg"),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print("\(i) finished downloading")
        }
    }
}

doSyncTaskInSerialQueue()

serialQueue.sync {
    for i in 0...3 {
        value = i
        print("\(value) in next block")
    }
}

print("last line in playground")

/*
 task running in main thread
 1 finished downloading
 task running in main thread
 2 finished downloading
 task running in main thread
 3 finished downloading
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 last line in playground
 */
  • 동기 상황의 스레드는 메인 스레드. 메인 스레드는 메인 큐에서밖에 실행할 수 없으므로 현 시점은 메인 큐
  • 동기 sync 블럭을 통해 실행한 디스패치 큐로 인해 메인 큐에서 실행되는 태스크는 현재 블락된 상황 → 즉 메인 스레드는 유휴 상태(idle)이므로 시스템은 이러한 메인 스레드를 활용하고자 함 → sync 블럭 내부에서 사용되는 스레드가 글로벌이 아니라 메인 스레드가 되는 까닭

Concurrent Queue Async

var value: Int = 20
let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)

func doAsyncTaskInConcurrentQueue() {
    for i in 1...3 {
        concurrentQueue.async {
            if Thread.isMainThread {
                print("task running in main thread")
            } else {
                print("task running in global thread")
            }
            guard
                let imageURL = URL(string: "https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg"),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print("\(i) finished downloading")
        }
    }
}

doAsyncTaskInConcurrentQueue()

concurrentQueue.async {
    for i in 0...3 {
        value = i
        print("\(i) in next block")
    }
}

print("last line in playground")

/*
 task running in global thread
 last line in playground
 task running in global thread
 task running in global thread
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 3 finished downloading
 1 finished downloading
 2 finished downloading
 */
  • 컨커런트 큐에서 비동기적으로 실행되는 태스크는 순서를 보장하지 않음

Concurrent Queue Sync

var value: Int = 20
let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)

func doSyncTaskInConcurrentQueue() {
    for i in 1...3 {
        concurrentQueue.sync {
            if Thread.isMainThread {
                print("task running in main thread")
            } else {
                print("task running in global thread")
            }
            guard
                let imageURL = URL(string: "https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg"),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print("\(i) finished downloading")
        }
    }
}

doSyncTaskInConcurrentQueue()

concurrentQueue.sync {
    for i in 0...3 {
        value = i
        print("\(i) in next block")
    }
}

print("last line in playground")

/*
 task running in main thread
 1 finished downloading
 task running in main thread
 2 finished downloading
 task running in main thread
 3 finished downloading
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 last line in playground
 */
  • 동기적으로 실행되는 스레드는 메인 스레드에서 실행
profile
JUST DO IT

0개의 댓글