본문 바로가기

iOS/Concurrency

[Swift] DispatchGroup으로 tasks 관리하기 | No6. GCD

728x90

 

안녕하세요.

이번 포스트는 DispatchGroup, CurrentPerform, DispatchPredition에 관한 개념을 정리하려고 합니다.

 

GCD 관련 포스트 개념을 정리했습니다.

 

 

Concurrency를 할 때 특정 task가 전부 끝날 경우 notification을 받고 싶은 경우가 있습니다. 예를들어 사용자가 로그인을 했을 때 해당 사용자의 정보, 프로필, 친구 관계, 영화, 승차권 등 예매했다면 예매 정보 등등 서버에 저장되어 있는 사용자에 관련된 정보를 전부 가져옵니다. 이때 사용자에 관한 정보들은 concurrency tool을 통해 async하게 받아옵니다.

 

각각의 task가 끝날 때마다 부분 부분 관련 데이터를 저장하는 곳에 업데이트 하는게 아니라 사용자에 관한 정보를 fetch하는 tasks가 전부 완료 되었을 때 notification을 준다면 쉽게 전부 받은 값들을 갱신할 수 있습니다.

 

Notification 하는 방법으로 notificationCenter와 옵저버를 달거나, delegate, rx, combine 등을 사용하는 경우가 있습니다. async/awiat를 이용하는 경우엔 async let 을 사용해 tasks를 관리할 수 있습니다. GCD를 사용한다면 tasks가 완료될 때 DispatchGroup를 통해 쉽게 관리할 수 있습니다.

 

1. What is DispatchGroup?

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

 

GCD는 DispatchGroup를 통해 비슷한 task들을 묶습니다. 각각의 task는 concurrency한 진행을 합니다. task들이 전부 완료했을 때 notification을 보낼 수 있습니다. 즉 dispatchGroup는 tasks를 추적할 수 있습니다. 그리고 group 내 모든 task가 완료되면 completion handler를 수행할 수 있습니다. 이때 각각의 task는 같은 queue에서 실행해도 되고 서로 다른 queue에서 실행해도 됩니다. 단지 goup으로 묶으면 됩니다.

 

let group = DispatchGroup()

 

dispatchGroup의 생성은 단순합니다.

 

group에 task를 추가하는 방법은 async(group:execute:)를 통해서 추가할 수도 있고 enter(), leave()를 통해 추가할 수 있습니다.

 

Use async(group:execute:)

/// 1. 
let imageProcessingQueue = DispatchQueue(
        label:"com.test.imageProcessing",
        attributes: .concurrent)

/// 2.
let group = DispatchGroup()

/// 3.
imageProcessingQueue.async(group: group) {
    print("task1")
}

/// 4.
let workItem = DispatchWorkItem {
    print("task2")
}

/// 5.
imageProcessingQueue.async(group: group, execute: workItem)

/// 6.
DispatchQueue.global().async(group: group) {
    print("task3")
}

 

1. task들을 수행할 커스텀 큐를 생성했습니다.

2. tasks를 관리 할 DispatchGroup을 생성합니다.

3. async(group:qos:flags:execute:)를 사용해 task를 queue에 추가합니다.

 

https://developer.apple.com/documentation/dispatch/dispatchqueue/2016098-async

 

  이때 task 추가하는 것은 () -> Void 타입인 클로저 형태로 async 사용하듯 추가하고 task를 group에도 추가합니다.

4. DispatchWorkItem을 사용해서 group에 추가하려고 합니다!! 마찬가지로 task를 DispatchWorkItem으로 감싸면 됩니다.

5. group추가!!

6. task들은 같은 큐 imageProcessingQueue가 아니라 global 형태의 다른 큐도 같이 group의 task에 추가할 수 있습니다.

 

Use enter(), leave() 

Group에 tasks 추가하는 또 다른 방법으로 enter(), leave()를 사용하겠습니다.

enter -> task -> leave 1대1 관계로 enter와 leave를 사용해야 합니다.

 

let imageProcessingQueue = DispatchQueue(
        label:"com.test.imageProcessing",
        attributes: .concurrent)
let group = DispatchGroup()

/// 3.
group.enter()
imageProcessingQueue.async {
    print("task1")
    
    /// 클로저의 맨 마지막
    group.leave()
}

/// 4.
group.enter()
let workItem = DispatchWorkItem {
    print("task2")
    group.leave()
}

/// 5.
imageProcessingQueue.async(execute: workItem)

 

group에 task를 추가하는 방법은 끝났습니다. 개인적으로는 async(group:execute:)를 통한 클로저로 task를 추가하는게 간편하다고 생각합니다.

 

2. When all tasks in the group are finished

group으로 tasks를 묶는 이유는 group안의 tasks가 전부 끝났을 때 completion handling을 할 수 있다는 점입니다. 두 가지 방법이 있습니다. 첫번째는 notify(qos:flags:queue:execute:)를 통해 notification을 받을 수 있습니다. 두번째는 wait()를 사용하는 것입니다.

 

1. notify(qos:flags:queue:execute:)

 

https://developer.apple.com/documentation/dispatch/dispatchgroup/2016066-notify

 

notify의 경우 @escaping키워드가 있습니다. work를 async한 수행할 수 있습니다. group의 모든 task가 끝나면 비동기적으로 notify's closure가 실행됩니다. Queue는 notify를 받을 work 클로저의 thread가 어느 queue의 thread에서 실행될 것인지를 지정할 수 있습니다.

 

.main의 경우 main thread에서 work 클로저가 실행이 됩니다. imageProfessingQueue면 커스텀 dispatchQueue가 할당한 thread에서 notify를 받을 수있습니다. 

 

///위의 코드 이어서

group.notify(queue: .main) {
    /// group의 모든 작업 완료 후 notify를 main queue에서 받습니다. queue를 지정하면 그에 맞는 thread가 수행합니다.
    /// tasks 완료시 이 클로저가 수행될 queue에서 배정된 thread!!
    
    if Thread.isMainTHread {
		print("메인 쓰레드에서 실행됩니다.")
	}else if Thread.isMultiThreaded() {
    	print("멀티 쓰레드에서 실행됩니다.")
    }
    
    print("group의 tasks 전부 완료! 이에 관한 추가 로직 처리")
}

 

2. wait()

/// group에 task 추가하는 코드
...
group.async(group: group) { ... }

group.wait()
print("group의 tasks 전부 완료! 이에 관한 추가 로직 처리")

 

group에 task를 전부 삽입한 후 group.wait()을 사용하게 된다면 current thread가 block됩니다.(group내 task가 전부 실행될 때 까지) wait 은 sync 함수입니다. 여기서 current thread는 group.wait()를 실행하는 thread를 의미합니다. 기본적으로 group.wait()를 사용하는 코드의 수행 thread가 DispatchQueue커스텀 큐나 global이 아니면 DispatchQueue main의 main thread에서 수행합니다. group.wait()를 사용하면 group의 모든 task가 완료될 때 까지 main thread가 block됩니다.

 

그 예로 ViewController의 viewdidLoad에서 확인할 수 있습니다.

extension ViewController {
    func imageProcessing() {
        let imageProcessingQueue = DispatchQueue(
            lable:"queue1",
            attributes: .concurrent)
        group = DispatchGroup()
        imageProcessingQueue.async(group: group) {
            print("task1")
        }
        imageProcessingQueue.async(group: group) {
            sleep(6)
            print("task2")
        }
        group.wait()
        /// group의 모든 task 완료. sync => 모든 그룹이 완료될 때 까지 해당 thread가 주시!! 
        ///(thread는 아래 코드 수행 x)  
        print("All task in group are finished")
    }
}

 

ViewController의 viewDidLoad

 

class ViewController: UIViewController {
	
    override func viewDidLoad() {
        super.viewDidLoad()
        imageProcessing()
        print("main thread block 해제.")
    }
}

 

main thread는 imageProcessing() 함수 호출 이후 group.wait()에 의해 group의 모든 task가 끝나기 전까지 block! 아래 코드( print("main thread block 해제"))를 수행하지 못합니다. group.wait를 실행하는 함수는 main thread이기 때문입니다.

 

class ViewController: UIViewController {
	
    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.global().async {
            self. imageProcessing()
        }
        print("main thread는 상관 없음.")
    }
}

 

반면 imageProcessing()의 group.wait() 코드를 실행하는 함수는 global에서 부여된 thread입니다. 해당 thread는 grup의 모든 task가 끝나기 전까지 다른 task 실행을 하지 못합니다.

 

3.  CurrentPerform

task를 작은 sub task로 나누어 동시에 실행해야 할 때 사용됩니다. Apple은 task를 전부 DispatchQueue's async()로 넣는 것 보다 currentPerform을 사용하는 것을 추천합니다. GCD는 쓰레드를 관리하는데 thread explosion이 발생할 수 있음으로 CurrentPerform을 사용하면 낫뱃!

for loop에서 사용하면 좋을 거 같습니다.

 

4.  DispatchPredition

어느 queue에서 실행되고 있는지 특정 조건을 통해 예측하는 것입니다. 근데 해당 큐에서 실행되는게 아닐 경우 앱이 멈추게 됩니다..

 

dispatchPrecondition(condition: .onQueue(.global()))

main thread에서 이 코드가 수행될 시 앱은 멈춥니다.

 

 

References:

https://shchukin-alex.medium.com/gcd-dispatchgroup-and-concurrentperform-f0e52706748

https://www.kodeco.com/28221123-grand-central-dispatch-tutorial-for-swift-5-part-2-2#toc-anchor-002

https://demian-develop.tistory.com/16

https://developer.apple.com/documentation/dispatch/dispatchgroup/

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

728x90