본문 바로가기

Swift/GCD

Multithreading with GCD 1 - Queues & Threads

GCD(Grand Central Dispatch)의 존재이유?

다양한 크기의 작업을 비동기적으로 실행하고 관리하는 데 사용되는 강력한 기능을 제공합니다. GCD를 사용하면 코드의 구현이 단순해지고, 성능이 향상될 수 있습니다.

Threads

Thread는 실행 중인 프로세스가 시스템 리소스를 효율적으로 분할하는 방법 중 하나입니다. 즉, 하나의 프로세스 내에서 여러 개의 스레드가 동시에 실행될 수 있으며, 각 스레드는 프로세스의 리소스를 공유하여 작업을 처리합니다. 이를 통해 다중 작업을 처리하고, 빠른 응답성과 성능을 보장할 수 있습니다.

iOS 앱은 여러 스레드를 활용하여 여러 작업을 실행하는 프로세스입니다. 장치에 있는 코어의 갯수만큼 많은 스레드를 활용할 수 있습니다. (앱의 특정 요구 사항과 하드웨어 리소스의 가용성에 따라 다를수 있습니다)

 

앱의 작업을 여러 스레드로 분할할 때 이점이 무엇일까요?

  1. 더 빠른 실행: 작업을 동시에 수행 할 수 있으므로 모든 것을 직렬로 실행하는 것 보다 더 빠르게 완료할 수 있습니다.
  2. 응답성: 기본 UI 스레드에서 사용자에게 보이는 작업만 수행한다면 사용자는 다른 스레드에서 수행하는 작업을 알지 못하고 이로 인해 느려지거나 정지되는 것을 모르게 됩니다.
  3. 최적화된 리소스 소비: 스레드는 OS에 의해 고도로 최적화됩니다.

Dispatch Queue

스레드를 통해 작업하는 방법은 DispatchQueue를 생성하는 것입니다. 큐를 생성하면, OS는 잠재적으로 하나 이상의 스레드를 만들고 queue에 할당합니다. 스레드가 이미 생성되어 있는 경우 재사용될 수 있습니다.

The main queue

앱이 시작되면 main dispatch queue는 자동으로 생성됩니다. 이것은 UI를 담당하는 직렬(serial) 큐입니다. 이것은 자주 사용하기 때문에 Apple 내에서 class variable로 생성되어 있으며 DispatchQueue.main으로 접근합니다. 실제 UI 작업과 관련되지 않는 한 main queue에 대해 동기식으로 무언가를 실행하는 것은 바람직하지 않습니다. 그렇지 않으면 잠재적으로 앱 성능을 저하시킬 수 있는 UI가 잠기게 됩니다. → Deadlock

Dispatch Queue에는 serial 또는 concurrent의 두 가지 타입이 있습니다. 기본 생성 타입은 serial 타입입니다.

main queue는 직렬형식의 큐이므로 다음 작업이 시작되기 전에 각 작업이 마무리되어야 합니다. concurrent queue를 생성하기 위해선 간단히 attribute 속성에 .concurrent를 추가하면 됩니다.

let label = "com.raywenderlich.mycoolapp.networking"
let queue = DispatchQueue(label: label, attributes: .concurrent)

concurrent queue는 서비스 품질 (Quality of service) 에 따라 6개의 서로 다른 global concurrent queue들을 제공합니다.

Quality of service (QOS)

concurrent queue를 사용하는 경우, iOS에 작업이 얼마나 중요한지를 알려줄 필요가 있습니다. 이를 위해 작업이 queue로 전송될 때 어떤 우선순위를 가져야 하는지를 지정하는 Quality of Service (QoS)를 사용합니다. 우선 순위가 높은 작업은 더 빨리 수행되어야 하며 완료하는 데 더 많은 시스템 리소스가 필요하고 우선순위가 낮은 작업보다 더 많은 에너지가 필요할 수 있습니다.

만약 concurrent queue가 필요하지만 직접 관리하고 싶지 않은 경우 global 클래스 메서드를 DispatchQueue에 사용하여 미리 정의된 global queue 중 하나를 가져올 수 있습니다.

 

💡 Global queue는 항상 동시적이고 선입선출 방식입니다.

let queue = DispatchQueue.global()
let userInteractivequeue = DispatchQueue.global(qos: .userInteractive)

위에서 언급했듯이 Apple은 6가지 QoS를 제공합니다.

.userInteractive

사용자가 직접 상호작용하는 일들에 권장됩니다. UI 업데이트 계산, 애니메이션 또는 UI 반응성과 속도를 유지하는 데 필요한 모든 것. 만약 일이 빠르게 일어나지 않으면, 얼어있는 것 처럼 보일 것입니다. 이 queue에 할당된 작업은 거의 즉시 완료되어야 합니다.

.userInitiated

사용자가 UI에서 즉시 발생해야 하지만 비동기적으로 수행 할 수 있는 작업을 시작할 때 사용해야 합니다. 예를 들어 문서를 열거나 로컬 데이터베이스에서 읽어야 할 때, 만약 사용자가 버튼을 눌렀을 때도 이 queue를 사용할 것입니다. 이 queue에서 수행되는 작업은 완료되는 데 몇 초 이하가 걸립니다.

.utility

일반적으로 장기 실행 계산, I/O, 네트워킹 또는 지속적인 데이터 피드와 같은 진행률 표시기가 포함되는 작업에 대해 사용하는 것이 좋습니다. 이 시스템은 반응성 및 성능과 에너지 효율성의 균형을 맞추려고 합니다. 작업은 이 queue에서 몇 초에서 몇 분 정도 걸릴 수 있습니다.

.background

사용자가 직접 알지 못하는 작업의 경우 사용합니다. 사용자 상호 작용이 필요하지 않으며 시간에 민감하지 않습니다. 미리 가져오기, 데이터베이스 유지 관리, 원격 서버 동기화 및 백업 수행 등이 모두 좋은 예입니다. OS는 속도 대신에 에너지 효율성에 초점을 맞춥니다. 대략 몇 분 이상 상당한 시간이 걸리는 작업에 이 queue을 사용하는 것이 좋습니다.

.default, .unspecified

다른 두 가지 선택 사항이 있지만 명시적으로 사용해서는 안됩니다. .default 옵션은 .userInitiated와 .utility 의 중간입니다. 그리고 qos 인자의 기본값입니다. 이것은 직접 사용하기 위한 것이 아닙니다. .unspecified은 서비스 품질에서 스레드를 선택할 수 있는 레거시 API를 지원하기 위해 존재합니다.

 

QoS 추론

고유한 concurrent dispatch queue를 생성하는 경우, 시스템에 QoS가 무엇인지 알릴 수 있습니다.

let queue = DispatchQueue(label: label,
                          qos: .userInitiated,
                          attributes: .concurrent)

그러나 이것은 항시 바뀔 수 있기 때문에 필요하지 않을 수 있습니다. OS는 queue에 할당되는 작업 유형에 주의를 기울이고 필요에 따라 변경합니다.

queue보다 서비스 품질이 높은 작업을 제출하면 queue의 수준이 높아집니다. 뿐만 아니라 queue에 추가된 모든 작업의 우선순위도 높아집니다.

 

QoS와 OS간 리소스 할당 관계는 추후에 다시 파볼 예정

 

Queue에 작업 추가

Dispatch queue는 sync와 async 두 가지 메서드를 제공하여 queue에 작업(task)을 추가합니다. Task는 “실행할 코드 블록들”입니다. 예를 들면 앱이 시작되면 앱 상태를 업데이트하기 위해 서버에 접속해야 할 수 있습니다. 이는 사용자가 시작한 것이 아니고 즉시 발생할 필요가 없으며 네트워킹 I/O에 따라 달라지므로 global utility queue에 보내져야 합니다.

DispatchQueue.global(qos: .utility).async { [weak self] in
  // 비동기작업 ~~ 
  // ...

  // main queue로 switch back
  // UI 업데이트
  DispatchQueue.main.async {
    self.textLabel.text = "New articles available!"
  }
}

 

UI를 업데이트할 때는 백그라운드 queue 내부에서 Dispatched.main 큐에서 업데이트를 합니다. 

Task를 sync하게 dispatch queue에 할당할 때 주의를 가져야 합니다. 현재 큐를 차단하는 현재 queue에 대한 작업을 sync하게 할당하고 작업이 현재 queue의 리소스를 접근하려 하면 앱이 교착상태가 되어버립니다. → Deadlock

비슷하게 main 큐에서 sync를 호출하면 UI를 업데이트하는 스레드가 차단되고 앱이 정지된 것 처럼 보이게 될 것입니다.

DispatchWorkItem

DispatchQueue에 일을 할당하는 방법에 익명의 클로저를 할당하는 방법 외에도 또 다른 방법이 있습니다.

DispatchWorkItem은 queue에 할당하려는 코드를 보관할 실제 객체를 제공하는 클래스입니다.

예를 들어,

let queue = DispatchQueue(label: "xyz")
queue.async {
  print("The block of code ran!")
}

위 코드를

let queue = DispatchQueue(label: "xyz")
let workItem = DispatchWorkItem {
  print("The block of code ran!")
}
queue.async(execute: workItem)

와 같이 클로저를 캡슐화할 수 있습니다.

이를 사용하는 이유는 workItem을 취소할 수 있다는 것입니다. 만약 cancel() 메서드를 호출한다면 다음 두 작업 중 하나가 수행됩니다.

  1. 만약 task가 아직 queue에서 시작되지 않았다면, 삭제될 것입니다.
  2. 만약 task가 현재 실행중이라면, isCanclled 프로퍼티가 true로 바뀔 것입니다.

당신은 코드에서 isCancelled 프로퍼티를 주기적으로 체크하고 가능하면 작업을 취소하기 위한 적절한 조치를 취해야 합니다.

또한, DispatchWorkItem클래스는 notify(queue:execute:) 메서드를 제공합니다. 이는 다른 DispatchWorkItem에 현재 작업이 끝나고 난 다음 실행되어야 한다고 알릴 수 있습니다.

let queue = DispatchQueue(label: "xyz")
let backgroundWorkItem = DispatchWorkItem { }
let updateUIWorkItem = DispatchWorkItem { }

backgroundWorkItem.notify(queue: DispatchQueue.main,
                          execute: updateUIWorkItem)
queue.async(execute: backgroundWorkItem)

 

 

 

 

출처

https://www.kodeco.com/books/concurrency-by-tutorials/v2.0/chapters/3-queues-threads