티스토리 뷰

iOS

GCD: Grand Central Dispatch

cskime 2020. 7. 27. 00:32

최근 면접준비를 하면서 'GCD에 대해 설명해보세요'라는 질문을 받는다면 제대로 대답하지 못할 것 같았다. 그래서 이번 기회에 시간을 들여 GCD(Grand Central Dispatch)와 Dispatch Framework에 대해 정리해보려고 한다. 기술을 공부할 때 항상 공식 문서를 보는 습관을 들이기 위해, 이번에도 공식 문서를 기반으로 공부해서 정리한다. 그리고, 제목마다 문서 링크를 달아놓았으니 추가적으로 공식 문서를 읽어보는 것을 추천한다.


모두 정리하고 보니 분량이 너무 많아져서 사용 방법만 간략히 요약해 둔다. 글을 모두 읽어주면 정말 감사하겠지만, 빠르게 사용법만 파악하고 싶은 독자는 이 부분만 봐도 된다. 하지만 시간을 들여서 모두 정독해 보는 것을 추천한다.

 

1. Dispatch queue 생성

// 생성

let serialQueue = DispatchQueue(label: "cskime.serialQueue")
let concurrentQueue = DispatchQueue(label: "cskime.concurrentQueue")

// 지연 실행

let inactiveQueue = DispatchQueue(label: "cskime.inactiveQueue", attributes: [.initiallyInactive])
...
inactiveQueue.activate()  // queue 활성화

2. System dispatch queue 사용

DispatchQueue.main.sync { ... }  // 가능한 조합이지만 main thread에서 sync는 사용하면 안됨
DispatchQueue.main.async { ... }
DispatchQueue.global().sync { ... }   // 가능한 조합이지만 serial-sync와 같은 효과이므로 잘 사용하지 않음
DispatchQueue.global(qos: .userInteractive).async { ... }  // qos 지정

3. Dispatch work item 사용

let workItemA = DispatchWorkItem { print("A") }
let workItemB = DispatchWorkItem { print("B") }
let workItemC = DispatchWorkItem { print("C") }
let queue = DispatchQueue.global()

// 1. 현재 thread에서 바로 실행. sync
workItem.perform()

// 2. WorkItem을 queue에 전달하여 실행
queue.async(execute: workItemA)

// 3. WorkItem의 지연 실행
queue.async(execute: workItemB)
workItemB.wait()  // workItemB가 끝날 때 까지 코드 실행 대기
print("Work Item B Completed")

// 4. WorkItem의 completion handler 추가 -> 순차적인 실행

workItemA(queue: queue, execute: workItemB)
workItemB(queue: queue, execute: workItemC)

// A -> B -> C 순서로 각 work item이 완료되면 다음 work item 실행

4. Dispatch group의 사용

let group = DispatchGroup()
let queue = DispatchQueue.global()

// 1. enter, leave를 사용한 counting

group.enter()
queue.async {
  print("A")
  group.leave()
}

// 2. Queue에 sync / async로 작업을 전달할 때 지정

queue.async(group: group) {
  print("B")
}

// 3. Group의 모든 작업 완료 시 completion handler 실행

// wait : 동기적 실행
group.wait()
print("Completed")

// notify: 비동기적 실행
group.notify(queue: queue) {
  print("Completed")
}

 


Dispatch Framwork

Dispatch는 멀티코어 하드웨어 환경에서 코드를 동시적으로 실행시키기 위한 Framework이다. 실행시킬 작업을 시스템이 관리하는 DispatchQueue에 전달하는 것만으로 동시성 프로그래밍이 가능해진다.

GCD라고도 부르는 Dispatch Framework는 macOS, iOS, watchOS, tvOS 등의 멀티 코어 하드웨어에서 동시성 프로그래밍을 위한 체계적이고 종합적인 기능 개선 사항을 제공하는 언어적 특성, 런타임 라이브러리, 시스템 향상 등의 내용을 포함하고 있다. 

 

BSD subsystem(OS X 등 UNIX 계열의 변형된 시스템. 관련자료), Core Foundation, Cocoa API 등은 모두 이러한 향상된 기능을 통해 시스템 및 application이 더 빠르고, 효율적이고, 향상된 응답성을 가질 수 있도록 확장되었다. 단일 application에서 여러 개의 core를 효율적으로 사용하는 것은 매우 어려웠는데, system level에서 동작하는 GCD는 사용 가능한 시스템 자원을 균형잡힌 상태로 실행중인 application들에 더욱 잘 매칭시켜줄 수 있다.

 

(이부분은 번역하기가 어려웠는데, GCD를 사용하면 시스템 레벨에서 사용 가능한 자원을 알아서 할당해줌으로써 이전에 프로그래머가 직접 자원을 할당하던 것 보다 쉽다 정도로 이해하면 될 것 같다. 틀렸거나 더 정확한 의미를 알고 있다면 댓글로 남겨주길 바란다.)

 

정리하면, GCD는 멀티 코어 환경에서 동시성 프로그래밍을 지원하기 위한 프레임워크이다. 기존에 동시성 프로그래밍을 위한 멀티 쓰레딩 프로그래밍에서, 개발자가 직접 Thread를 생성, 관리 등의 책임을 가졌다면 GCD를 사용함으로써 이런 책임을 시스템 레벨로 넘길 수 있다는 것이다. Thread 생성, 실행 등의 관리 책임을 운영체제로 넘김으로써 개발자는 무엇을 실행할 것인가(What)에 더 집중할 수 있게 된다. 

 

이 글에서는 Dispatch Framework에서 Queue와 Task에 관련된 다음 세 가지 객체의 사용 방법에 대해서 알아본다.


DispatchQueue

DispatchQueue는 작업들을 application의 main thread 또는 background thread에서 순차적으로(serially) 또는 동시에(concurrently) 실행시키는 것을 관리하는 객체이다.

Dispatch queue는 FIFO 특성을 갖는 Queue로, application은 block object(closure)의 형태로 queue에 작업을 전달할 수 있다. Dispatch queue는 작업들을 순차적으로(serially) 또는 동시에(concurrently) 실행시킨다. Dispatch queue에 전달된 작업은 시스템에 의해 관리되는 thread pool에서 실행된다. Application의 main thread를 나타내는 dispatch queue를 제외하고, 시스템은 어떤 thread가 작업을 실행할 지 알 수 없다.

 

DispatchQueue에 작업 객체(work items)를 전달할 때는 동기적(Synchronously) 또는 비동기적(Asynchronously)으로 작업을 예약하게 된다. 작업을 동기적으로 실행하도록 하면, 코드는 작업의 실행이 끝날 때 까지 기다린다. 작업을 비동기적으로 실행하도록 하면, 코드는 작업이 실행되는 동안 (종료를 기다리지 않고) 계속 실행된다.

 

여기서, 작업을 순차적으로(serially) 실행시키는 dispatch queue를 SerialQueue, 동시에(concurrently) 실행시키는 dispatch queue를 ConcurrentQueue라고 부른다. SerialQueue와 DispatchQueue에 작업을 전달할 때, 해당 작업의 실행 방식을 Sync 및 Async로 지정할 수 있게 된다. Serial, Concurrent, Synchronous, Asynchronous의 개념이 햇갈린다면 이전 글(iOS | DispatchQueue로 Serial/Concurrent와 Sync/Async 이해하기)을 참고하길 바란다.

 

정리하면,

 

  1. Application은 block object 형태의 작업들을 DispatchQueue에 전달한다.
  2. DispatchQueue는 Serial 및 Concurrent 방식으로 작업을 thread에 전달하여 실행시킨다.
  3. 작업을 Queue에 전달할 때, 작업의 실행을 동기(Sync) 및 비동기(Async) 방식으로 예약(schedule)한다.
  4. Main Queue에 전달된 작업들은 main thread에서 실행된다. 그 외 다른 queue에 전달된 작업들은 실행될 thread를 알 수 없다.

 

이 때, 'Main Queue에 전달된 작업들은 동기(Sync)적으로 실행시키면 안된다'는 것에 주의해야 한다. Main Queue는 시스템에서 생성하는 Serial Queue로, 작업들을 main thread에서 실행시킨다. Main thread는 application의 main run loop가 동작하는 곳으로, 시간이 오래걸리는 작업을 동기적으로 실행시키게 되면 application이 멈춰버리는 결과를 초래한다.

 

Main run loop에서 동작해야 하는 작업 중 대표적인 것이 UI 업데이트와 관련된 작업들이다. 앱이 실행되면 UIApplication 객체를 생성하고 main run loop 및 event loop가 동작하게 되는데, 이것들은 모두 main thread에서 동작한다. UI를 그리고 event를 처리하는 작업들은 이 run loop에서 매 주기마다 실행되기 때문에, UI 업데이트와 관련된 작업들은 반드시 main thread에서 실행되어야 한다.

 

이와 관련하여 실수하기 쉬운 것이 이미지를 URL로부터 요청해서 가져온 다음 ImageView에 넣어주는 작업을 할 때이다. URLSession의 dataTask를 통해 이미지를 요청하는 작업은 background thread에서 실행되기 때문에, completionHandler에서 data를 UIImage로 변환하여 ImageView에 넣으면 crash가 발생한다. ImageView가 image를 화면에 보여주는 작업은 UI 업데이트에 관련된 작업이기 때문에 별도로 main queue에서 작업해야 한다.

 

URLSession.shared.dataTask(url: imageURL) { (data, response, error) in 
  // Error Handling and Check HTTPResponse...
  
  guard let data = data, let image = UIImage(data: data) else { return }
  
   DispatchQueue.main.async {
     imageView.image = image
   }
}.resume()

 

UITableView를 사용할 때 tableview의 갱신 작업도 주의해야 한다. 서버에 데이터를 요청하여 새로운 데이터로 tableview를 갱신하는 상황을 생각해보자. 별도의 method에서 API에 요청을 보내고, 응답으로 들어온 데이터를 completionHandler를 통해 전달하는 경우, completionHandler에서 tableView.reloadData()를 실행시키려면 반드시 main thread에서 실행되도록 해야 한다.

 

HTTPHandler.request(url: url, completionHandler: { (result) in 
  switch result {
  case .success(let data):
    self.dataSource = data
    
    // TableView 갱신은 main thread에서 실행
    DispatchQueue.main.async {
      self.tableview.reloadData
    }
  case .failure(let error):
    print(error.localizedDescription)
  }
})

 

정리하면, UI 업데이트 관련 작업은 MainQueue에 전달하여 main thread에서 실행되도록 해야 한다. 그 밖에 시간이 오래 걸릴 수 있는 작업들은 GlobalQueue를 사용하거나 ConcurrentQueue를 생성하여 실행시켜야 한다.


다음으로 dispatch queue를 사용하는 방법을 알아보자. Dispatch queue는 생성자를 통해 직접 생성할 수도 있고 시스템에서 생성해 둔 Queue를 사용할 수도 있다.

Custom Dispatch Queue

작업 block을 전달할 수 있는 새로운 dispatch queue를 생성한다. 이렇게 생성한 Queue에 전달되는 작업들은 main thread를 제외한 나머지 background thread에서 실행된다.

생성자는 5가지 인자를 받는데, 그 중 'label', 'qos', 'attributes' 을 살펴보자. 나머지 인자는 특별한 경우가 아니라면 기본값으로 사용한다.

 

  • label : Instruments, sample, stackshots, crash report 등의 디버깅 툴 사용 시 Queue를 고유하게 식별하기 위한 문자열. Applications, libraries, frameworks 등은 모두 자기 자신의 dispatch queue를 생성해서 갖고 있기 떄문에, reverse-DNS(com.example.myqueue) 형태의 이름을 사용하는 것을 추천한다
  • qos : Queue의 qos(quality-of-service) level. 시스템이 실행시킬 작업을 예약할 때 우선순위를 결정한다. DispatchQoS.QoSClass에 정의된 값이 들어갈 수 있다.
  • attributes : Queue에 관한 속성값. ConcurrentQueue를 생성하는 `.concurrent` 속성을 포함한다. 아무런 값도 전달하지 않으면 SerialQueue가 생성된다. '.initiallyInactive' 속성은 비활성 상태의 dispatch queue를 생성하고 activate() method를 호출하여 작업을 실행시킨다.

사용 예:

 

let serialQueue = DispatchQueue(label: "cskime.serialQueue")
serialQueue.async { ... }

let concurrentQueue = DispatchQueue(label: "cskime.concurrentQueue", attributes: [.concurrent])
concurrentQueue.async { ... }

let inactiveQueue = DispatchQueue(label: "cskime.inactiveQueue", attributes: [.initiallyInactive])
inactiveQueue.async { ... }

// Somewhere
inactiveQueue.activate()  // execute

 


System Dispatch Queue

시스템이 생성한 Queue로, SerialQueue인 main queue와 ConcurrentQueue인 global queue가 있다.

 

Main queue는 현재 프로세스에서 main thread와 관련된 dispatch queue 이다. DispatchQueue의 타입 프로퍼티 'main'을 통해 접근할 수 있다.

시스템은 자동으로 main queue를 생성하고 main thread에 연결한다. Application은 main queue에 전달된 작업들을 실행시키기 위해 다음 세 가지 방법 중 한 가지를 사용한다.

 

1. dispatchMain() 호출

2. UIApplicationMain(_:_:_:_:) 함수 호출(iOS에서)

3. Main thread에서 CFRunLoop를 사용합니다.

 

각 함수에 대한 설명은 글의 논지를 흐린다고 생각되어 별도로 다루지 않았다. 대신 각각 문서 링크를 달아놓았으니 참고하길 바란다. 요약하면, 세 가지 중 하나의 함수를 호출함으로써 main thread에서 event run loop가 동작하고 main queue에 전달되는 작업을 실행할 준비를 하게 된다. 

 

만약, application이 네트워크 호출 같은 오랫동안 실행되는 작업을 한다면, global system queue 또는 다른 background dispatch queue에서 실행해야 한다. 아니면, 가능한 경우 비동기 호출을 사용해야 한다.

(UI 업데이트 이외의 작업들은 main queue가 아닌 다른 queue를 사용하고, main queue에서는 sync를 호출하지 않는 등 위에서 말한 주의사항에 관한 내용이다.)

 

사용 예:

DispatchQueue.main.async { ... }

'global(qos:)'는 dispatch queue의 타입 method로, 지정된 qos(quality-of-service)로 작업을 실행시키는 global system queue를 반환한다.

Global queue는 main thread를 제외한 다른 background thread에서 실행되며 ConcurrentQueue로 생성된다. ConcurrentQueue가 필요하다면 별도로 생성하지 않아도 global queue를 사용할 수 있다.

 

사용 예:

DispatchQueue.global().async { ... }                // Use default qos
DispatchQUeue.global(qos: .utility).async { ... }   // Use utility qos

 


DispatchQoS(Quality of Service)

QoS(Quality-of-Service)는 작업 실행의 우선순위를 지정하는 서비스 품질(quality of service)에 대한 분류이다. 

QoS를 사용하여 application이 수행하려는 작업의 의도를 전달하면, 시스템은 QoS를 통해 사용 가능한 자원들 내에서 작업들을 실행하는 최적의 방법을 결정한다. 예를 들어, 시스템은 사용자와 상호작용하는 작업(user-interactive task)을 포함하고 있는 thread들에 높은 우선순위를 부여하여 그 작업이 빠르게 실행되도록 한다. 반대로, background 작업들에는 더 낮은 우선순위를 부여하고, 전력 효율이 좋은 CPU 코어(power-efficient CPU core)에서 실행되도록 하여 전력 효율을 높이려고 시도할 것이다. 시스템은 시스템 및 예약된 작업들의 상태에 따라 작업들을 동적으로 실행시킬 방법을 결정한다.

 

높은 우선 순위를 갖는 작업은 더 빨리 실행되고 더 많은 리소스와 에너지를 사용하기 때문에, 빠르고 에너지 효율이 높은 앱을 만들기 위해서는 적절한 QoS를 지정하여 우선 순위(qos)를 매겨야 한다. QoS는 다음 6가지로 분류되고, 왼쪽으로 갈 수록 작업에 높은 우선순위가 부여된다.

 

User Interactive > User Initiated > Default > Utility > Background > Unspecified

 

1. User Interactive

 

애니메이션, 이벤트 처리, UI 업데이트 등 사용자와 상호작용하는 작업들을 위한 분류이다. 사용자 경험을 높이기 위해 즉각적으로 반응해야 하므로, 시스템에서 가장 높은 우선순위를 갖는다.

 

2. User Initiated

 

사용자에게 작업의 결과를 즉각적으로 제공해야 하거나, 잠시동안 사용자가 앱을 적극적으로(actively) 사용하지 못하게 막는 작업들을 위한 분류이다. 시스템에서 두 번째로 높은 우선 순위를 갖는다. 사용자에게 보여줄 이메일 내용을 불러오는 경우에 사용할 수 있다. User-Interactive와 마찬가지로 반응성과 성능에 중점을 두지만 비교적 요청에 대한 응답이 시간이 소요될 수 있는 작업에 사용한다.

 

3. Default

 

QoS의 기본값으로, User-Initiated보다 낮고 Utility보다 높은 우선 순위를 갖는다. 앱을 시작하거나 사용자를 대신하여 활발한 작업(active work)을 수행하는 작업 또는 queue에 할당한다.

 

4. Utility

 

사용자가 적극적으로 확인하거나 추적하지 않는 작업을 위한 분류이다. Default 레벨보다 낮지만 Background 보다는 높은 우선 순위를 갖는다. 사용자가 신경쓰지 않는 오래 걸리는 작업 등에 할당한다. 우선 순위가 비교적 낮지만 빠른 시간 내에 결과를 반환해야 하는 작업을 위한 것이며, 성능과 전력 효율을 효과적으로 관리할 수 있다.

 

5. Background

 

계속 유지해야 하거나 정리(clean up)해야 하는 작업들을 위한 분류이다. 가장 낮은 우선 순위를 갖는다. 동기화, 백업 등 앱이 백그라운드에서 동작하는 동안 수행해야 하는 작업들에 할당한다.

 

6. Unspecified

 

QoS 정보를 제공하지 않는다. 이 때는 시스템이 QoS를 추론한다.

 


DispatchWorkItem

DispatchWorkItem은 dispatch queue나 dispatch group에서 실행하기 위해 수행하려는 작업을 감싸고 있는 객체이다. DispatchQueue에 block object 대신 work item을 통해 작업을 전달할 수 있다.

DispatchWorkItem은 작업의 실행과 관련된 몇 가지 기능들을 제공한다.

 

'perform()' method를 실행하면 현재 thread에서 작업을 곧바로 실행시킨다. Work item의 작업이 끝나야 다음 코드를 실행한다.

 

let workItem = DispatchWorkItem {
  print("A")
}

workItem.perform()
print("B")

// Prints
// A
// B

 

'wait()` method를 사용하면 비동기로 실행되는 작업이더라도 해당 작업이 종료될 때 까지 다음 코드의 실행을 기다린다(동기적으로 실행시킨다).

 

let workItem = DispatchWorkItem {
  for _ in 0...1_000_000 { }
  print("A")
}

DispatchQueue.global().async(execute: workItem)
workItem.wait()
print("B")

// Prints
// A
// B

 

 'wait(timeout:)' method를 사용하여 대기하는 시간(timeout)을 설정하면 작업이 종료되지 않았어도 timeout에서 설정한 시간이 지나면 다음 코드를 실행한다.

 

let workItem = DispatchWorkItem {
  for _ in 0...1_000_000 { }
  print("A")
}

DispatchQueue.global().async(execute: workItem)
workItem.wait(timeout: .now() + 0.1)
print("B")

// Prints
// B
// A

 

'cancel()' method를 사용하여 작업의 실행 전 또는 실행 중에 작업 실행을 취소시킬 수 있다. 이 때, 'cancel()' method는 비동기적으로 호출해야 한다. 만약, 'cancel()' method를 호출한 시점이 실제로 작업이 실행되기 전이라면 작업은 실행되지 않는다. 하지만, 작업이 실행된 다음에 'cancel()'을 호출하게 된다면 해당 작업의 내부에서 work item의 'isCancelled' flag를 검사하여 취소되었을 때 코드를 실행하지 않도록 해야 한다.

 

var workItem: DispatchWorkItem?
workItem = DispatchWorkItem {
  let bigNumber = 8_000_000
  let divideNumber = bigNumber / 4
  for i in 1...bigNumber {
    guard i % divideNumber == 0 else { continue }
    guard let workItem = workItem, !workItem.isCancelled else { return }
    print(i / divideNumber * 25, "%")
  }
}

// WorkItem의 작업 실행 후 약 3초 뒤에 취소시킴
DispatchQueue.global().async {
  guard let timeoutResult = workItem?.wait(timeout: .now() + 3) else { return }
  switch timeoutResult {
  case .success:  print("Success")
  case .timedOut:
    workItem?.cancel()
    print("TimeOut")
  }
}

 

'notify(queue:execute:)` method를 사용하여 현재 실행되고 있는 work item이 종료된 다음에 특정 work item을 이어서 실행시킬 수 있다. 연달아 실행시킬 작업들에 사용할 수 있다. 'execute'에 이어서 실행시킬 새로운 work item을 전달하고 'queue' 인자에 work item을 실행시킬 queue를 전달한다.

 

let workItemA = DispatchWorkItem {
  for _ in 0...1_000_000 { }
  print("A")
}

let workItemB = DispatchWorkItem {
  for _ in 0...1_000_000 { }
  print("B")
}

let workItemC = DispatchWorkItem {
  for _ in 0...1_000_000 { }
  print("C")
}
let workItemD = DispatchWorkItem {
  for _ in 0...1_000_000 { }
  print("D")
}

let globalQueue = DispatchQueue.global()
globalQueue.async(execute: workItemA)
workItemA.notify(queue: globalQueue, execute: workItemB)
workItemB.notify(queue: globalQueue, execute: workItemC)
workItemC.notify(queue: globalQueue, execute: workItemD)

// Prints
// A
// B
// C
// D

 


DispatchGroup

DispatchGroup은 여러 개의 작업들을(group of tasks) 하나의 유닛으로 관찰할 수 있게 해 주는 객체이다.

Dispatch group은 일련의 작업들을 하나로 묶고, group 내에서 동작을 동기화(synchronize)시킨다. 여러 개의 work item을 group에 붙이고, 같거나 다른 queue에서 비동기적으로 실행시키게 된다. Group에 묶인 모든 작업들이 끝나면, group은 completion handler를 실행시킨다. 또는, Group 안에 모든 작업들이 끝나기를 동기적으로 기다릴 수도 있다.

 

Dispatch group은 전달 인자 없이 인스턴스를 생성한다. Dispatch group은 묶여 있는(associated) 작업들 중 실행 중(outstanding)인 작업의 개수를 관리하는데, 새로운 작업이 group에 포함되면 증가하고 작업이 완료되면 감소한다. 이후 'notify(queue:execute:)` 또는 'wait()` 같은 method를 사용해서 application이 group 내의 모든 작업이 완료되었음을 알리고, application은 적절한 행동(action)을 하게 된다.

 

DispatchGroup에 작업 연결하기(attach)

'enter()''leave()'를 사용해서 각각 group에 포함될 작업의 시작과 종료를 명시적으로 나타낼 수 있다. 이 방법은 'enter()`를 호출함으로써 dispatch group의 작업 count를 증가시키고, 'leave()'를 호출함으로써 count를 감소시킨다.

 

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "concurrent1", attributes: [.concurrent])
let queue2 = DispatchQueue(label: "concurrent1", attributes: [.concurrent])

group.enter()
queue1.async {
  for _ in 0..<1000000 { }
  print("A")
  group.leave()
}

group.enter()
queue2.async {
  for _ in 0..<1000 { }
  print("B")
  group.leave()
}

group.notify(queue: queue1) {
  print("Completed 1")
}

group.notify(queue: queue1) {
  print("Completed 2")
}

// Prints
// B
// A
// Completed 1
// Completed 2

 

또는, 작업이 전달되는 queue에서 sync 또는 async method에 group을 전달하여 해당 queue에 전달되는 작업들이 group으로 묶일 것임을 알려줄 수 있다. 이 방법은 queue에 작업이 전달될 때 count를 증가시키고, 그 작업이 종료되었을 때 count를 감소시킨다.

 

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "concurrent1", attributes: [.concurrent])
let queue2 = DispatchQueue(label: "concurrent1", attributes: [.concurrent])

queue1.async(group: group) {
  for _ in 0..<1000000 { }
  print("A")
}

queue2.async(group: group) {
  for _ in 0..<1000 { }
  print("B")
}

group.notify(queue: queue1) {
  print("Completed 1")
}

group.notify(queue: queue1) {
  print("Completed 2")
}

// Prints
// B
// A
// Completed1
// Completed2

DispatchGroup 내의 모든 작업이 완료되었을 때 action 구현

'wait()'와 'notify()' method를 사용하여 DispatchGroup에 연결된 작업들이 모두 완료되었을 때 callback 동작을 구현할 수 있다. 이 둘의 차이는 callback을 각각 동기/비동기 방식으로 실행시킨다는 것에 있다.

 

'wait()'는 group 내의 모든 작업이 종료되기를 동기적으로 기다린 뒤 다음 코드를 실행한다. 'wait()' method를 호출한 시점에서 코드 실행이 멈추고, 모든 작업이 완료된 후 다음 코드를 실행한다.

 

let group = DispatchGroup()
let queue = DispatchQueue.global()

queue.async(group: group) {
  for _ in 0..<1000000 { }
  print("A")
}

queue.async(group: group) {
  for _ in 0..<1000 { }
  print("B")
}

group.wait()  // 모든 작업이 완료되기를 기다린다.

print("Completed 1")

print("Next Task")

// Prints
// B
// A
// Completed 1
// Next Task

 

'wait(timeout:)'는모든 작업이 완료되지 않았더라도 설정한 timeout만큼 시간이 지나면 다음 코드를 실행한다.

 

let group = DispatchGroup()
let queue = DispatchQueue.global()

queue.async(group: group) {
  for _ in 0..<1000000 { }
  print("A")
}

queue.async(group: group) {
  for _ in 0..<1000 { }
  print("B")
}

group.wait(timeout: .now() + 0.1)

print("Completed 1")

print("Next Task")

// Prints
// B
// Completed 1
// Next Task
// A

 

'notify()'는 group 내의 모든 작업이 종료되었을 때 비동기적으로 completion handler를 실행시킨다. 'notify()' method를 호출한 시점에서 group 내의 모든 작업이 완료되었을 때 호출할 callback block을 등록해 두고, 바로 다음 코드를 실행한다.

 

import Foundation

let group = DispatchGroup()
let queue = DispatchQueue.global()

queue.async(group: group) {
  for _ in 0..<1000000 { }
  print("A")
}

queue.async(group: group) {
  for _ in 0..<1000 { }
  print("B")
}

group.notify(queue: queue) {
  print("Completed 1")
}

print("Next Task")

// Prints
// Next Task
// B
// A
// Completed 1

 

이처럼 dispatch group은 여러 개의 비동기 작업들을 마치 하나의 작업처럼 묶어서 처리할 수 있게 해 준다.

 

 

'iOS' 카테고리의 다른 글

RxSwift를 시작하기 전에  (2) 2020.08.11
Frame과 Bounds의 차이  (0) 2020.08.07
GCD: Grand Central Dispatch  (0) 2020.07.27
Serial/Concurrent와 Sync/Async 이해하기(feat. DispatchQueue)  (0) 2020.07.26
Application Life Cycle  (0) 2020.07.15
ViewController Life Cycle  (0) 2020.07.13
댓글
댓글쓰기 폼