지난 포스팅에서 알아본 GCD

Dispatch Queue에 작업을 추가하면

해당 Queue에 연결된 Thread로 작업을 분배해 준다.

 

하지만, 비동기인 작업의 경우에 해당 작업을 기다리지 않기에 끝나는 시점을 알지 못한다.

이번 포스팅에서 알아볼 Dispatch Group은 작업이 끝나는 시점을 알 수 있다. 

 

 

DispatchGroup

이름에서 알 수 있듯이, DispatchGroup은 일련의 비동기적으로 수행할 작업(Task)을 그룹화를 한다.

여기서 주의할 점은 비동기적으로 수행할 작업만 그룹화 할 수 있으며, 동기적으로 수행할 작업은 그룹화할 수 없다.

 

그룹화된 작업들은 각각 서로 다른 DispatchQueue를 통해 수행될 수 있다.

즉, 서로 다른 Thread에서 수행될 수 있다.

또한, DispatchGroup의 모든 작업이 완료될 때 completion handler를 실행하여, 일련의 작업의 완료 시점을 알 수 있다. 

 

우선 DispatchGroup의 생성은 다음과 같다.

let group1 = DispatchGroup()

 

이제 그룹에 작업을 추가해 보자! 

func task(_ taskNumber: Int) {
    print("task\(taskNumber)")
}
DispatchQueue.global().async(group: group1) {
    task(1)
}
DispatchQueue.global().async(group: group1) {
    task(2)
}

DispatchQueue.global(qos: .userInteractive).async(group: group1) {
    task(3)
}
DispatchQueue.global(qos: .userInitiated).async(group: group1) {
    task(4)
}

 

 

task(1), task(2)는 default global Dispatch Queue에, 

task(3)은 userInteractive global Dispatch Queue에, 

task(4)는 userInitated global Dispatch Queue를 통해 다른 Thread에서 실행하는 작업을 추가하였다.

 

 

작업이 완료되면, notify()execute(completion handler)의 호출을 통해 작업의 끝나는 시점을 알 수 있다. 

추가적으로 notify() 메서드에서 excute(completion handler)가 실행될 Queue를 지정할 수 있다.

public func notify(
            qos: DispatchQoS = .unspecified,
            flags: DispatchWorkItemFlags = [],
            queue: DispatchQueue,
            execute work: @escaping @convention(block) () -> Void)
group1.notify(queue: .main) { //main Queue에서 excute 실행
    print("task finish")
}

/* print:
 task2
 task4
 task3
 task1
 task finish
*/

 

결과를 보면 다른 Thread에서 비동기적으로 실행되기 때문에 순서는 다르지만, 

모든 작업이 끝났을 때 completion handler가 호출된 것을 알 수 있다. 

 

 

wait()

지금까지는 asyn하게 기다렸다면, wait() 메서드를 통해 동기적으로 작업을 기다릴 수 있다. 

즉, 그룹의 작업이 모두 수행될 때까지, 혹은 지정한 시간 동안 

해당 작업을 수행하고 있는 Thread를 블록처리하고 기다린다.

public func wait()
//모든 작업이 끝날때까지 기다림

public func wait(timeout: DispatchTime) -> DispatchTimeoutResult
//timeout만큼 기다리고 해당 시간안에 작업을 모두 수행하면 .success를 아니면 .timedOut을 반환한다.

public func wait(wallTimeout timeout: DispatchWallTime) -> DispatchTimeoutResult
// 위의 메서드와 비슷하지만, 
// DispatchTime은 나노세컨드의 정확도를 가지고 있고, DispatchWallTime은 마이크로 세컨드의 정확도를 가지고 있다고 한다.
DispatchQueue.global().async(group: group1) {
    sleep(2)
    task(1)
}

let successCase = group1.wait(timeout: .now() + 1)

switch successCase {
case .success:
    print("success")
case .timedOut:
    print("timedOut")
}

/* print:
 timedOut
 task1
 task finish
*/

timeOut이 되었다고, 작업의 종료를 명시하는 것이 아닌, 

Thread의 독점, 

즉, 동기적 기다림이 종료되었다는 것을 의미한다.

 

 

enter(), leave()

URLSession과 같이 작업의 끝나는 시점을 모르거나, 

비동기적 작업인 경우

즉! 작업 자체가 비동기인 경우에는 원치 않는 결과가 출력될 수 있는데, 

아래와 같이 completion handler가 작업이 끝나기 전 시점에 호출될 수 있다.

DispatchQueue.global().async(group: group1) {
    task(1)
    DispatchQueue.global(qos: .userInteractive).async{
        sleep(3)
        print("비동기 작업 완료!")
    }
}

group1.notify(queue: .main) {
    print("task finish")
}

/* print:
 task1
 task finish
 비동기 작업 완료!
*/

 

group1에 추가된 작업은 동기적 작업인 task(1)빨간색 블럭으로 표시한 비동기적 작업으로 구성되어 있다.

=

이때 빨간색 블럭은 비동기적 작업이기 때문에 

작업의 완료를 기다리지 않고 리턴이 된다.

이에 따라, group1에 추가된 작업은 완료되어

빨간색 블럭의 작업이 완료되기 전에 completion handler가 호출된다.

이를 해결하기 위해 enter(), leave() 메서드를 통해 manually하게 작업의 시작과 종료 시점을 알릴 수 있다.

enter()메서드는 카운트를 증가시키고, leave() 메서드는 카운트를 감소시키며, 카운트가 0이 되면 completion handler가 호출된다. 

group1.enter()

DispatchQueue.global().async(group: group1) {
    task(1)
    DispatchQueue.global(qos: .userInteractive).async{
        sleep(3)
        print("비동기 작업 완료!")
        
        group1.leave()
    }
}

/* print:
 task1
 비동기 작업 완료!
 task finish
*/

 

위에서 언급했듯이, URLSession과 같이 끝나는 시점을 명확히 모를 경우

해당 블럭에서 마지막에 실행한다는 의미인 defer키워드를 사용하여 leave()를 호출한다. 

 

@IBAction func buttonTapped(_ sender: Any) {
   
    let group = DispatchGroup()

    group.enter()
    URLSession.shared.dataTask(with: URL(string: imageURL[0])!) { [weak self] data, _, err in
        defer { group.leave() }
        
        guard let strongSelf = self, err == nil, let data = data else { return }
        
        DispatchQueue.main.async {
            strongSelf.imageView1.image = UIImage(data: data)
        }
        
    }.resume()
    
    group.enter()
    URLSession.shared.dataTask(with: URL(string: imageURL[1])!) { [weak self] data, _, err in
        defer { group.leave() }

        guard let strongSelf = self, err == nil, let data = data else { return }
        
        DispatchQueue.main.async {
            strongSelf.imageView2.image = UIImage(data: data)
        }
        
    }.resume()
    
    group.enter()
    URLSession.shared.dataTask(with: URL(string: imageURL[2])!) { [weak self] data, _, err in
        defer { group.leave() }

        guard let strongSelf = self, err == nil, let data = data else { return }
        
        DispatchQueue.main.async {
            strongSelf.imageView3.image = UIImage(data: data)
        }
        
    }.resume()
    
    group.notify(queue: .main) { [weak self] in
        guard let strongSelf = self else { return }
        
        strongSelf.finishLabel.text = "downLoad Done"
    }
}

 

 

Reference

DispatchGroup

younggyun님의 블로그

복사했습니다!