[Swift] Grand Central Dispatch


Grand Central Dispatch


본 포스팅에 대한 내용은 이전 Concurrency Programing 포스팅 내용의 연장된 심화 내용 입니다.

Dispatch Group

다양한 스레드에서 비동기적으로 작업을 그룹 짓고, 각 그룹별로 끝나는 시점을 알고싶을 때 주로 사용

let group1 = DispatchGroup( )

DispatchQueue.global(qos: ).async(group: group1) {
    // code
} 
DispatchQueue.global(qos: ).async(group: group1) {
    // code
}
DispatchQueue.global( ).async(group: group1) {
    // code
}

// 메인 스레드에 알리고 작업해야함
group1.notify(queue: DispatchQueue.main) { [weak self] in
    self?.textLabel.text = "모든 작업이 완료되었습니다."
}

// 언제까지 기다릴 수 없을 때 사용하는 메서드
if group1.wait(timeout: .now( ) + 60 ) == .timedOut {
    print("작업이 60초안에 종료하지 않았습니다.")
}

  • 동기적인 기다림
    • 어떤 이유로 그룹의 완료 알림에 비동기적으로 응답 할 수 없는 경우, 대신 디스패치 그룹에서 wait 메서드를 사용할 수 있음
      • 실제앱에서 메인큐(메인쓰레드)에서는 wait 메서드 사용하면 안됨
      • 메인큐(메인쓰레드)가 아닌 이미 다른큐(다른쓰레드)에서는 wait 메서드 사용가능할 수 있음
    • 모든 작업이 완료 될 때까지 현재 대기열을 차단하는 동기적 방법
    • 작업이 완료 될 때까지, 얼마나 오래 기다릴지 기다리는 시간을 지정하는 선택적(optional) 파라미터가 필요 (지정하지 않으면 무제한으로 대기)

Async DispatchQueue Group

DispatchQueue.async(group: group1) {
    group1.enter( )   // 함수 시작
    someAsyncMethod { 
        // Async code
        group1.leave( )   // 함수 끝
    }
}
  • 디스패치 그룹 클로저 내에서 비동기함수를 호출할때 실제 태스크가 끝나지 않았는데 비동기적으로 처리하는 과정에서 클로저가 종료되면서 태스크도 종료되어 버릴 수 있음
  • 함수의 시작과 끝을 알려주는 .enter(), .leave()의 짝이 맞으면 디스패치 그룹이 비로소 종료됨
    • .enter(), .leave()는 Reference Counting을 사용

Dispatch Work Item

  • 작업을 미리 정의해 놓고 사용하는, 큐에 제출하기 위한 객체
  • 빈약한 <취소 기능=""> `cancel()` 메서드
    • 작업이 아직 시작 안된 경우(아직 큐에 있을때) 작업이 제거
    • 작업이 실행중인 경우 isCancelled 속성이 true로 설정
      (직접적으로 실행중인 작업이 멈추는 것은 아님)
  • 빈약한 <순서 기능=""> `notify(queue: 실행할큐, execute: 디스패치아이템)` 메서드
    • 직접적으로 실행 다음에, 실행할 아이템(작업)을 지정

Dispatch Semaphore

  • 공유 리소스에 접근가능한 작업 수를 제한해야할 경우
    ( ex. 다운로드 숫자를 제한해야 할때)
// 현재 쓸 수 있는 자원의 수 3개로 제한
let semaphore = DispatchSemaphore(value: 3)

DispatchQueue.async(group: group1) { 
    group1.enter( )   // 함수 시작
    semaphore.wait( )  // 가용 자원 수 -1
    someAsyncMethod { 
        group1.leave( )   // 함수 끝
        semaphore.signal( )   // 가용 자원의 수 +1
    }
}

Problems of Concurrency 동시성과 관련된 문제

  • 2개이상의 쓰레드를 사용하면서, 동일한 메모리 접근 등으로 인해 발생할 수 있는 문제
  • 실행시마다 항상 같은 순서로 발생하는 것이 아니기 때문에, 디버그 하기 어려움

Race Condition 경쟁 상황

var a = 1

DispatchQueue.global( ).async {   // a에 접근
    sleep(1)
    a += 1 
}
DispatchQueue.global( ).async {   // a에 접근
    sleep(1)
    a += 1
}

// 비동기적으로 a에 접근했을 때 a는 제대로 프린트 되지 않음
print(a)
  • 두개 이상의 쓰레드가 한 곳의 메모리(저장공간)에 동시에 접근하여 값을 사용하려고 할때 문제가 발생할 수 있음
    • 읽는 것은 동시에 해도 괜찮을 수 있으나, 읽기/쓰기가 동시에 이루어 진다던지 쓰기 작업 여러개가 동시에 이루어지는 것은 Thread-safe 하지 않음
  • lazy var를 통해서 변수를 생성할 때도 지연 저장 속성의 특성 상 경쟁 상황이 발생할 수 있음

TSan

  • Thread-Sanitizer를 통해 잠재적 경쟁 상황을 찾을 수 있음

Thread-Safety

  • 해결 방안 : Thread-safe(쓰레드 세이프)
    • 여러 쓰레드가 동시에 쓰여도 안전함
    • 동시적 처리를 하면서(여러 스레드를 사용하면서도) 문제없이 스레드를 안전하게 사용

      데이터(객체나 변수 등)에 여러 쓰레드를 사용하여 접근하여도, 한번에 한개의 쓰레드만 접근가능하도록 처리하여 경쟁상황의 문제없이 사용

Dispatch Barrier

concurrentQueue.async(flags: .barrier) {    
    // Barrier Task
}
  • concurrent큐 내의 여러개 쓰레드 중에서 배리어 작업의 경우, 한개의 쓰레드만 사용해 serial(직렬)로 실행가능한 방법
    • concurrent큐임에도 불구하고, 시리얼하게 동작
    • 보낸 쓰레드는 기다리지 않고 비동기적으로 동작
    • 주로는 메인쓰레드가 아닌 쓰레드에서 다시 접근할때 Barrier 의미가 있음

객체 설계

  • 객체에 접근할때 메인 큐(쓰레드)가 아닌 다른 큐에서 접근할 가능성이 있는 지 확인 후 객체 내부에 Thread-safe처리
    • Serial Queue + sync
    • Barrier 처리

Deadlocks 교착 상태

2개이상의 쓰레드가 2개이상의 배타적인 자원 사용으로 인해 서로 점유하려고 하면서 자원사용이 막혀 작업이 이러지도 저러지도 못하고 진행이 되지 않는 상태

  • 다양한 발생 가능성
    • 동기 작업이 현재의 쓰레드가 필요한 경우
    • 앞선 작업이 현재의 쓰레드가 필요한 경우
    • 여러개의 세마포어가 존재할때, 순서 잘못 설계 등
  • 보통 교착 상태는 시리얼 큐로 해결 가능
  • 세마포어나 제한된 리소스 순서 같은 것들을 조심히 사용해야하고, 객체 등 설계 시에 각별히 주의해야 함

Priority Inversion 우선 순위 역전

낮은 우선 순위의 작업이 자원을 배타적으로 사용하고 있을 때, 다른 작업이 자원을 사용하지 못하게 막고 있으므로 작업의 우선 순위가 바뀔 수 있음

  • 다양한 발생 가능성
    • 시리얼큐에서 높은 우선순위 작업이 낮은 우선순위의 뒤에 보내지는 경우
    • 낮은 우선순위의 작업이 높은 우선순위가 필요한 자원을 잠그고 있는 경우
      (ex. lock코드, 세마포어 등)
    • 높은 우선순위 작업이 낮은작업에 의존하는(디펜던시) 경우 (Operation)
  • 1차적으로 GCD가 우선순위를 조정해서 알아서 해결
    (실직적으로는 낮은 우선순위 작업의 우선순위를 높여서 우선처리)
  • (안전하게) 공유된 자원 접근시 - 동일한 QoS 사용

Async / Sync / Serial / Concurrent

Async vs Sync

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

concurrent vs serial

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

Serial + Sync

DispatchQueue.main( ).sync {
    // main 큐에서 동기 방식으로 실행
}

메인 스레드의 작업 흐름이 queue에 넘긴 태스크가 끝날때까지 멈춰있고(sync)
넘겨진 태스크는 queue에 먼저 담겨있던 작업들과 같은 스레드에 보내지기 때문에
해당 작업들이 모두 끝나야 실행 (Serial Queue)

serial + Async

DispatchQueue.main( ).async {
    // main 큐에서 비동기 방식으로 실행
}

메인 스레드의 작업 흐름이 태스크를 queue에 넘기자마자 반환되고 (async)
넘겨진 task는 queue에 먼저 담겨있던 작업들과 같은 스레드에 보내지기 때문에
해당 작업들이 모두 끝나야 실행 (Serial Queue)

Concurrent + Sync

DispatchQueue.global( ).sync {
    // background 큐에서 동기 방식으로 실행
}

메인 스레드의 작업 흐름이 queue에 넘긴 태스크가 끝날때까지 멈춰있고(sync)
넘겨진 task는 queue에 먼저 담겨있던 작업들과 다른 스레드에 보내질 수 있기 때문에
해당 작업들이 모두 끝나지 않아도 실행 (Concurrent Queue)

Concurrent + Async

DispatchQueue.global( ).async {
    // background 큐에서 비동기 방식으로 실행
}

메인 스레드의 작업 흐름이 태스크를 queue에 넘기자마자 반환되고 (async)
넘겨진 task는 queue에 먼저 담겨있던 작업들과 다른 스레드에 보내질 수 있기 때문에
해당 작업들이 모두 끝나지 않아도 실행 (Concurrent Queue)

주의 사항

main 큐에서는 sync 메서드 사용 금지

  • 메인 쓰레드에서 sync 메서드를 호출하게되면 끊임없이 앱의 이벤트 처리가 대기상태로 멈추게 되면서 교착 상태에 빠지고 모든 앱의 이벤트 처리가 멈추게 됨
  • 꼭 사용해야 할 상황이 생긴다면 async 큐로 보낸 뒤 그 안에서 sync 큐로 보내는 방법을 활용해야 함
    • 그러나, sync메서드는 경쟁 상황을 피하는데는 매우 유용
    • 큐가 Serial 큐이고 객체에 접근하는 유일한 방법이라면, sync 메서드는 메인 쓰레드가 아닌 모든 쓰레드가 일관된 값을 얻는 것을 보장하면서 실행

같은 global 큐 에서 async 큐 안에 sync 작업 호출 금지

  • async 큐 안에 sync 큐를 사용할때 같은 global 큐에 sync 작업을 보낸다면 교착 상태가 발생
  • QoS 설정을 통해 다른 큐를 사용하는 것이 좋음

참고

Ellen iOS Concurrency Class
Dumb-veloper tistory Blog
김종권의 iOS tistory Blog
sujinnaljin medium Blog






© 2021. by hminkim