본문 바로가기

Swift/GCD

Multithreading with GCD 2 - Groups & Semaphores

DispatchGroup

DispatchGroup 클래스는 task 그룹의 완료를 추적할 때 사용합니다.

DispatchGroup을 정의하고, dispatch queue의 async 메서드에 인자로 그룹을 제공하여 해당 그룹의 일부로 작업을 추적할 수 있습니다.

let group = DispatchGroup()

someQueue.async(group: group) { ... your work ... } 
someQueue.async(group: group) { ... more work .... }
someOtherQueue.async(group: group) { ... other work ... } 

group.notify(queue: DispatchQueue.main) { [weak self] in
  self?.textLabel.text = "All jobs have completed"
}

위 예제 코드에서 볼 수 있듯이 그룹은 단일 dispatch queue에 고정으로 연결되어 있지 않습니다.

단일 그룹으로 사용할 수 있지만, 실행해야 하는 작업의 우선순위에 따라 여러 queue에 작업을 할당할 수 있습니다. DispatchGroup은 할당된 모든 작업이 완료되는 즉시 알림을 받기 위할 때 notify(queue: ) 메서드를 사용합니다.

 

💡 notification은 그 자체로 비동기식이기 때문에, 이전에 할당된 작업이 아직 완료되지 않은 한 알림을 호출하고 난 뒤에도 추가로 작업을 할당할 수 있습니다. 그렇지만 notify() 메서드가 호출되고난 다음에는 작업을 추가하더라도 그룹의 상태가 변경되어 모든 작업이 이미 완료되었다는 것을 알리는 내부 플래그가 설정된 상태이기 때문에 notify()가 다시 호출되지는 않습니다.

 

DispatchGroup 클래스는 여러 개의 작업들이 모두 완료되었는지를 추적하며, 모든 작업이 완료되면 특정 작업을 실행할 수 있도록 도와줍니다. notify 메서드는 모든 작업이 완료되었을 때 실행될 클로저와, 그 클로저를 실행할 디스패치 큐를 인자로 받습니다.

notify 메서드에는 QoS(Quality of Service) 값을 지정할 수 있는 다른 버전도 존재합니다.

Synchronous waiting

만약, 여러가지 이유로, asynchronously 한 그룹의 completion notification에 응답할 수 없다면, dipatch group에 wait 메서드를 사용할 수 있습니다. 이것은 synchronous 메서드로 작업을 마칠 때까지 현재 큐를 막습니다.

작업이 완료될 때 까지 기다리는 시간을 지정하는 optional 한 매개변수를 설정할 수 있습니다. 이를 지정하지 않으면 무한 대기시간이 생깁니다.

let group = DispatchGroup()

someQueue.async(group: group) { ... }
someQueue.async(group: group) { ... }
someOtherQueue.async(group: group) { ... } 

if group.wait(timeout: .now() + 60) == .timedOut {
  print("The jobs didn’t finish in 60 seconds")
}

위 코드는 작업이 반환되기 전 작업을 완료할 수 있도록 최대 60초를 할당합니다.

 

💡 이것은 현재 스레드를 막습니다. 그렇기 때문에 절대 main queue에서 wait을 호출하면 안 됩니다.

 

그리고 시간초과가 발생한 후에도 작업이 계속 실행되고 있다는 점을 알아야 합니다.

Wrapping asynchronous methods

DispatchQueue는 DispatchGroup과 함께 사용하여 작업 그룹화를 할 수 있습니다. 작업 그룹화를 하면, 여러 작업들이 완료될 때까지 기다릴 수 있고, 그룹 내 작업들이 모두 완료되면 지정한 작업을 실행할 수 있습니다.

하지만 만약 클로저 내부에서 비동기 메서드를 호출하면, 그 메서드가 완료되기 전에 클로저가 이미 완료되어 버릴 수 있습니다. 이런 경우, DispatchGroup에서 제공하는 enter와 leave 메서드를 사용하여 그룹에 속한 작업들이 완료될 때까지 그룹 내 작업의 진행 상황을 추적할 수 있습니다.

enter 메서드는 작업 그룹 내의 작업 개수를 1 증가시키고, leave 메서드는 작업 그룹 내의 작업 개수를 1 감소시킵니다. 따라서, 작업 그룹 내 작업이 모두 완료될 때까지 enter와 leave를 반복적으로 사용하여 그룹 내 작업의 상태를 추적하고, 작업 그룹이 완료될 때 적절한 작업을 실행할 수 있습니다.

queue.dispatch(group: group) {
  // count is 1
  group.enter()
  // count is 2
  someAsyncMethod { 
    defer { group.leave() }
    
    // Perform your work here,
    // count goes back to 1 once complete
  }
}

group.enter() 메서드를 호출하여 dispatch group에 실행 중인 다른 코드 블록이 있음을 알립니다.

위 그룹의 완료 상태는 someAsyncMethod의 완료 상태에 group.leave()의 호출에 달려있습니다. 그렇지 않으면 완료 신호를 받지 못할 것입니다. 오류 조건 중에도 호출해야 하기 때문에 defer 클로저를 통하여 종료하는 방법에 관계없이 group.leave()가 실행되도록 합니다.

Semaphores

공유 리소스에 액세스 할 수 있는 스레드 수를 실제로 제어해야 하는 경우가 있습니다. 단일 스레드에 대한 읽기/쓰기 패턴을 이미 보았지만 총 스레드 수에 대한 제어를 유지하면서 한 번에 더 많은 리소스를 사용할 수 있는 경우가 있습니다.

데이터를 네트워크에서 다운로드하는 경우, 한 번에 발생하는 다운로드 수를 제한할 수 있습니다.

dispatch queue를 사용하여 작업을 오프로드하고, 디스패치 그룹을 사용하여 모든 다운로드가 완료된 시점을 알 수 있습니다. 그러나 받는 데이터가 상당히 크고 처리할 리소스가 많다는 것을 알고 있기 때문에 한 번에 4개의 다운로드만 허용하려고 합니다.

DispatchSemaphore를 사용하면, 해당 사용사례를 완벽하게 처리할 수 있습니다. 리소스를 원하는 대로 사용하기 전에 synchronous 메서드인 wait 메서드를 호출하기만 하면 리소스를 사용할 수 있을 때까지 스레드가 실행을 일시 중지합니다. 만약, 스레드가 소유권을 주장한 것이 없으면 즉시 액세스할 수 있습니다. 다른 스레드가 가지고 있다면, 그들이 그것을 끝내고 signal 할 때까지 기다릴 것입니다.

semaphore를 만들 때, 리소스에 허용할 비동기 액세스들을 정의해야 합니다. 만약 한 번에 4개의 네트워크의 다운로드를 가능하게 하려면 semaphore에 4를 전달하고, 리소스에 대한 독점적인 접근을 허용하려면, 1을 전달합니다.

한 번에 4개의 네트워킹을 허용한 코드입니다.

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
for i in 1...10 {
  queue.async(group: group) {
      semaphore.wait()
      defer { semaphore.signal() }

      print("\\(i)번 이미지를 다운로드합니다.")

      // 네트워킹한다고 가정 (3초 소요)
      Thread.sleep(forTimeInterval: 3)

      print("(3초뒤) \\(i)번 이미지를 다운로드 완료했습니다.")

  }
}

 

먼저 dispatch group 사용 시 leave를 호출해줘야 하는 것처럼, 리소스를 사용하는 것을 완료하면 defer 블록을 통해 signal을 호출해 주어야 합니다.

 

출처

https://www.kodeco.com/books/concurrency-by-tutorials/v2.0/chapters/4-groups-semaphores