본문 바로가기

iOS/Concurrency

[Swift] GCD와 Concurrency. main thread와 dispatch queue global 개념 뿌수기!! | No4. GCD

 

안녕하세요. 

Swift의 concurrency. CGD의 개념을 공부했는데 GCD와 dispatchQueue global에 관해 정리하려고 합니다.

 

 

GCD를 소개하기 전에 concurrency의 기본 개념 Thread, async,sync, dispatchQueue등의 개념을 정리했습니다. 먼저 참고해주시면 감사합니다. GCD 관련 포스트 정리.

 

이전 포스트의 Thread파트에서 잠깐 소개했지만 Main Thread는 UIResponder 타입인 UIApplication과 Run loop를 통해 user interface를 담당합니다. Main Thread는 단 하나 존재합니다. UI 업데이트, 사용자의 터치, 제스처, event 반응 등 기본적으로 탐지하고 그에 맞는 델리게이트를 main thread에서 호출합니다.

 

서버로부터 받는 다량의 데이터, network, image processing등의 작업은 시간을 비교적 오래 소모합니다. DispatchQueue main은 serial queue입니다. 특정 시간대에 하나의 task만 수행할 수 있습니다. 이런 작업 모두 main thread에서 담당한다면 앱의 반응성은 낮아집니다. UI가 느려지거나 중지될 수 있습니다. 사용자의 기분은 다운됩니다. -> 앱 삭제 읗.. 

 

이런 문제 개선을 위해 Concurrency의 개념이 등장했습니다. (같은 시간에 여러 개의 task를 수행할 수 있다! Multi core이니까 main thread의 부담을 줄여주자!!) NSThread로 구현할 경우 thread safety를 유지하기 어렵습니다. NSOperationQueue는 초기에 GCD에선 할 수 없던 기능들을 제공하지만 구현이 복잡하다고 합니다. 그래서 나온 것이 GCD 입니다. 추가적으로 task는 GCD의 DispatchWorkItem으로 감싼다면 task cancel도 가능합니다. Sepahores, Barriors, Dispatch groups, 등이 지원됩니다.

 

 

1. What is GCD(Grand Central Dispatch)?

CGD는 concurrent 옵션을 관리하는 low-level C기반 API입니다 + asynchronous task working. 오래 걸리는 작업은 background의 Thread를 통해 main thread와 서로 다른 task를 동시에 수행한다는 개념을 갖고 있습니다. NSLock, NSThread보다 쉬운 model입니다. thread 위에 만들어졌습니다. 하지만 native하게 thread를 다루진 않습니다. 직접 thread를 제어하진 못하고 shared thread pool을 관리합니다. Queue에 task를 dispatch를 하면 이들을 어느 thread에서 수행해야 할지 GCD가 결정합니다. 물론 native model 이 나왔습니다. 그것은 Swift 5.5ver async/await +_+..

 

GCD는 system과 system resource에 따라 얼마나 많은 prallelism processing을 할지 결정합니다. parallelism을 한다는 것은 concurrency를 필요하다는 것인데, concurrency는 parallelism을 보장하진 않습니다. GCD는 Disptch Queue 클래스를 통해 운영됩니다. macOS, iOS 등 multicore hardware에서 concurrent code 지원에 대한 다양한 기능을 포함합니다.

 

GCD는 concurrency를 잘 활용할 수 있게 threads 위에 몇가지 추상적인 개념을 도입했습니다. 이를통해 asynchronous code, concurrency를 작동시킵니다. 그것은 바로 DispatchQueuesRunLoops입니다.

 

https://developer.apple.com/videos/play/wwdc2016/720/

 

dispatchQueue 는 memory관리, task의 pause, resume, task 정의, task data log 등을 합니다. 그리고 dispatchQueue는 이전 포스트에서 소개했지만 async, sync 두 함수가 있습니다. 이를 통해 task를 dispatch queue에게 보낼 수 있습니다. FIFO의 특징을 갖는 큐는 task가 들어온 순서대로 enqueue 합니다.

 

https://developer.apple.com/videos/play/wwdc2016/720/

dispatchQueue의 queue에게 task를 보낼 수 있는 두 가지 방법 async(), sync()는 각각 () throws -> Void, @escaping() -> Void로 클로저 형태입니다. 클로저의 타입을 반환하는 함수도 가능합니다. (queue에 추가하는 방법은 정말 단 두 가지!! async, sync). 그리고 위 사진과 같이 queue에는 task가 enqueue 됩니다.

 

https://developer.apple.com/videos/play/wwdc2016/720/

 

GCD가 dispatchQueue에 저장된 task를 수행하기 위해 dispatch는 thread(worker-실타래 뭉치)를 가져옵니다. disptch가 누구일까요? GCD입니다. thread는 task를 수행합니다. dispatch queue에 있는 task가 Thread에서 작업되어 다 끝나게 된다면 thread 스스로 해제됩니다. 관련 기능은 이전 포스트 마지막 부분 custom queue가 thread와 어떤 관련이 있는지 사진을 통해 볼 수 있습니다.

 

dispatch에 의해 불러와지는 thread 말고 custom thread 또한 만들 수 있습니다. thread를 만들 땐 주의할 점이 run loop가 자동으로 생성되지 않는 다는 점입니다.(추가 해줘야 합니다.)

 

2. Main thread?

코드의 흐름은 top-down입니다. 임의로 지정하지 않는 이상 대부분의 코드 실행은 main thread에서 일어납니다. UIApplication 인스턴스 초기화부터 UI(UserInterface)를 그리는 로직은 main thread system resource, 시간을 들여 수행합니다. 예를 들어 tableView's cell 인스턴스 생성과 초기화, UI draw. 최근엔 120Hz까지 지원을 했습니다. 화면의 내용이 1초에 몇 번 바뀌는지에 대한 refresh rate 관련 작업,컬랙션뷰의 초기화 등.. main thread가 하는 걸 어떻게 알 수 있을까요? 디버깅을 통해 알 수 있지만 Thread.isMainThread인지 코드 실행 중간중간에 체크를 해도 알 수 있습니다.

 

그렇기에 dispatchQueue를 사용해야 한다는 것입니다. asynchronous를 통한 concurrency.

https://developer.apple.com/documentation/xcode/improving-app-responsiveness

 

3. How to use disptchQueue's task?

dispatchQueue의 async, sync 함수 설명은 이전 포스트에 다뤘습니다. 참고해 주시면 감사합니다! 

간단한 코드로는

/// 1. or let background = DispatchQueue.global(qos: .background)
let background = DispatchQueue(label "concurrencyTask")
/// 2.
background.async {
	/// 3.
	let smallImage = image.resize(to: rect)
    /// 4.
    DispatchQueue.main.async{
		imageView.image = smallImage
	}
    
}

 

1. global or custom queue으로 DispatchQueue 생성. global의 경우 concurrency type, custom의 경우 default = serial.

2. queue에 task 추가(use async or sync case by case :)

3. task의 내용 정의. 많은 시간이 소요되는 복잡한 계산, network request, data fetch, image processing, codable 등 task 정의.

이때 DispatchQueue는 개별적인 thread를 할당받고 main Queue의 main thread와 같이 concurrent 수행을 합니다. 

4. DispatchQueue.main.async를 통해 main thread에 비동기적으로 image 업데이트.

 

4. DispatchQueue's queue type

  • Main queue // serial
  • Global queue // concurrency
  • Custom queue // serial or concurrency

 

이 중 이전 포스트에서 다루지 않은 Global queue에 관해 개념을 정리합니다.

DispatchQueue's queue type: global

background에서 dispatchQueue에서 task를 수행하기 위해 선정된 Thread들은 main thread와 함께, 같은 시간대에 동시에 수행하기 위한 목적이 있습니다. concurrent queue입니다. 여러 thread가 실행된다는 의미는 곧 여러 task가 실행된다는 의미입니다. 그리고 전체 system에서 공유합니다. 하지만 몇 개의 작업이 실행되는지, 특정 task이후 next task는 언제 실행되는지 수행여부는 알 수 없습니다. 각각의 task 수행 시작은 GCD에 의해 결정됩니다.

 

main queue와 global queue는 system에 의해 제공된 것입니다. global queue는 Swift's operating system(중에 GCD)에서 관리됩니다. 그래서 '우선순위' 기능을 제공해 어느 task가 우선적으로 실행되야 하는지를 결정합니다. 

 

Quality of service

우선순위가 높을수록 빠르게 처리되야 합니다. 비교적 낮은 우선순위의 task 실행에 사용되는 자원, 비용, 에너지를 더 많이 사용합니다. 핵심적인 것은 task에 올바른 종류의 priority를 부여해야 합니다. system resource는 한정적이기 때문입니다. (적절히 잘 사용해야 합니다!!) 우선순위가 낮은 작업의 resource가 점유하고 있을 때 우선순위가 높은 task에게 resource가 재할당 될 수 있습니다. 높은 우선순위는 낮은 우선순위보다 먼저 실행되야 하기 때문입니다.

 

https://developer.apple.com/videos/play/wwdc2016/720/

User Interactive가 우선순위가 가장 높습니다. Background는 가장 낮은 우선순위에 해당합니다. WWDC에서 소개된 4개의 우선순위 외에도 두 가지 종류의 QoS가 있습니다. 그중 default는 User Initiated, User Interactive보단 낮고 Utility, Background보단 높습니다.

 

  • User-Interactive

사용자가 좋은 경험을 할 수 있도록 바로 완료되야 하는 task. 작은 작업 부하, 애니메이션, 앱 인터페이스 등의 UI update, event handling 등에 사용되야 합니다. 상호작용과 관련된 task를 담당해서 앱의 실행 중인 동안에 task complete 양이 작습니다. UI로직 처리 이후 UI update는 main thread에서 업데이트해야 합니다!!

  • User-Initiated

UI에서 발생한 async task 수행합니다. 사용자가 즉각적인 결과를 기다릴 때, 사용자와 상호 작용하는 것을 지속할 때 사용자가 앱을 클릭한 뒤 action 처리, api로부터 데이터 fetch 등의 작업 수행에 적합합니다.

  • utility

오랜 시간을 소요하는 작업. prograss indicator가 화면에 등장하면서 상대적으로 오랜 작업이 걸리는 작업에서 사용됩니다. networking, 계산, I/O, 지속적인 데이터 공급 등과 같은 작업일 때 부여합니다.

  • background

사용자에게 보이지 않는, 인식하지 못하는 task에 해당합니다. 사용자와 상호작용이 일어나지 않고 시간에 민감하지 않은 미리 가져오기, db에 데이터 저장, 백업, 유지 관리 등에 사용됩니다.

 

 

.async에 qos를 부여하면 됩니다. 그래야 concurrency 하게 사용되니까요!

let queue = DispatchQueue(label: "queue", attributes: .concurrent)
queue.async(qos: .background) {
	// 데이터 다운 등의 로직
}

 

한 가지 추가로.. dispatchQueue의 background에서 비동기적으로 작업된 task가 화면에 보일 때 너무 빨리 보이면 사용자가 미처 눈치채지 못할 수도 있습니다. 그럴 땐 asyncAfter를 사용해서 특정한 시간 뒤에 이미지를 보여주는 것을 delay 함으로 사용자에게 이미지 받아졌어! 하는 느낌을 보여줄 수 있습니다.

 

포스트 내용 중 틀린 개념 발견 시 댓글로 알려주시면 정말 감사합니다.

 

참고:

https://developer.apple.com/videos/play/wwdc2016/720/

https://developer.apple.com/videos/play/wwdc2017/706/

https://developer.apple.com/videos/play/wwdc2015/718/

https://www.kodeco.com/28540615-grand-central-dispatch-tutorial-for-swift-5-part-1-2

https://velog.io/@enebin777/Swift-GCD-queue%EB%93%A4-%EA%B9%8A%EC%9D%B4-%EB%93%A4%EC%97%AC%EB%8B%A4%EB%B3%B4%EA%B8%B0

https://developer.apple.com/documentation/xcode/improving-app-responsiveness

https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html#//apple_ref/doc/uid/TP40015243-CH39-SW1