본문 바로가기
아이폰 개발/Swift

swift DispatchGroup 과 DispatchSemaphore - 1

by 인생여희 2021. 9. 25.

swift DispatchGroup 과 DispatchSemaphore - 1

 

 

ios 에서는 여러작업 (task)을 처리하기 위해서는 어디로 작업을 보내야하나? 

 

바로 queue (대기행렬) 이다.

작업을 queue에 보내면  알아서 os가 다른 스레드로 분산처리를 해주도록 한다.

 

main thread 에 쌓인 task를 queue에 보내기만 하면 , queue 가 다른 스레드를 적절히 생성해서 분배해준다. 

 

 

queue에 쌓인 task는 누가 처리하나? 

 

"GCD"

 

GCD는 Queue에 작업을 보내면 스레드를 적절히 생성해서 분배해주는 역할을 한다.

그리고 GCD에서 사용하는 queue의 이름이 Dispatch Queue 이다.

즉, Dispatch Queue에 작업을 추가하면 GCD는 작업에 맞는 스레드를 자동으로 생성해서 실행하고, 작업이 종료되면 스레드를 제거한다.

 

"DispatchQueue = 큐에 보내다. "

 

출처 : https://sujinnaljin.medium.com

 

 

 queue에 작업을 보내고 난 후 메인 스레드는 어떤 행동을할까?

1.신경 끄고 자기한테 쌓여있는 다음 일을 한다 (비동기)

2.끝날 때까지 기다린 후에 자기한테 쌓여있는 다음 일을 한다 (동기)

 

DispatchQueue.global().async {

  //task

}

 

DispatchQueue: iOS에서 동시성 프로그래밍을 돕기 위해 제공하는 queue

global: DispatchQueue의 종류

async: 비동기

 

global dispatch queue에 비동기로 task를 보낸다.

원래의 작업이 진행되고 있던 메인 스레드에서 global dispatch queue로 task를 보낸 후, 해당 작업이 끝나기를 기다리지 않고 이어서 할 일을 한다.

 

queue.async { task } 또는 queue.sync { task } 를 통해 task를 queue로 보낸 후에 GCD의 행동은?

 

1.한개의 스레드에 몰빵한다. (직렬처리)

2.여러개의 스레드에 분배해준다. (병렬처리)

 

큐의 특성 : Serial(직렬)와 Concurrent(동시)

 

직렬 : 메인 스레드에서 분산 처리 시킨 작업을 “다른 한개의 스레드에서” 처리하는 큐 가 바로 Serial(직렬) Queue 이다.

동시 : 보통 메인 스레드에서 분산 처리 시킨 작업을 “다른 여러개의 스레드에서” 처리하는 큐 가 바로 Concurrent(동시) Queue 이다.

 

어떤 Queue를 사용할 것인가?

어떤 큐를 사용할 것인지에 대한 핵심 포인트는 바로 작업 순서의 중요도에 있다.

 

직렬: 순서가 중요한 작업

동시:속도가 중요한 작업

 

 

async(비동기) vs sync(동기)

작업을 보내는 시점에서 기다릴지 말지에 대해 다루는

 

concurrent(병렬) vs serial (직렬)

Queue(대기열) 보내진 작업들을 여러개의 스레드로 보낼 것인지 한개의 스레드로 보낼 것인지에 대해 다루는

 

출처:https://sujinnaljin.medium.com

 

 

4개의 상황(경우)

SerialQueue.sync : 메인 스레드의 작업 흐름이 queue에 넘긴 태스크가 끝날때까지 멈춰있고(sync), 넘겨진 task는 queue에 먼저 담겨있던 작업들과 같은 스레드에 보내지기 때문에 해당 작업들이 모두 끝나야 실행 (Serial Queue)

 

ConcurrentQueue.sync : 메인 스레드의 작업 흐름이 queue에 넘긴 태스크가 끝날때까지 멈춰있고(sync), 넘겨진 task는 queue에 먼저 담겨있던 작업들과 다른 스레드에 보내질 수 있기 때문에 해당 작업들이 모두 끝나지 않아도 실행 (Concurrent Queue)

 

SerialQueue.async : 메인 스레드의 작업 흐름이 태스크를 queue에 넘기자마자 반환되고 (async), 넘겨진 task는 queue에 먼저 담겨있던 작업들과 같은 스레드에 보내지기 때문에 해당 작업들이 모두 끝나야 실행 (Serial Queue)

 

ConcurrentQueue.async : 메인 스레드의 작업 흐름이 태스크를 queue에 넘기자마자 반환되고 (async), 넘겨진 task는 queue에 먼저 담겨있던 작업들과 다른 스레드에 보내질 수 있기 때문에 해당 작업들이 모두 끝나지 않아도 실행 (Concurrent Queue)

 

세가지 종류의 DispatchQueue

 

Main Queue

  • 오직 한개만 존재
  • Serial 특성을 가진 Queue
  • 이곳에 할당된 task는 메인 스레드에서 처리 (UI 업데이트 내용 처리)

Global Queue

  • Concurrent 특성을 가진 Queue
  • Qos (Quality Of Service)에 따라 여러개의 종류로 나뉨 (6종류)

 

Custom Queue

  • 커스텀으로 만듦
  • 디폴트로 Serial 특성을 가진 Queue. 하지만 Concurrent 로 설정 가능.
  • Qos 설정 가능

 

sync 메소드에 대한 주의 사항 2가지

 

1. 메인 큐에서 다른 큐로 작업을 보낼 때 sync를 사용하면 안된다

메인 스레드에서는 항상 async (비동기) 로 작업을 보내야한다.

 

2. 현재와 같은 큐에 sync로 작업을 보내면 안된다

현재와 같은 큐에 sync로 작업을 보내면 데드락 상황이 발생할 수도 있기 때문에 같은 큐에 sync 로 작업을 보내지 말아야 한다.

글로벌 큐는 Qos에 따라 각각 다른 큐 객체를 생성한다.

즉 DispatchQueue.global(qos: .utility) 와 DispatchQueue.global()는 다른 큐이다.

따라서 각각 다른 Qos 큐라면 쓰레드가 겹칠일이 없기 때문에 데드락 발생 가능성이 없다.

 

 

Dispatch Group

여러 스레드로 분배된 작업들이 끝나는 시점을 각각 파악하는 것이 아니라, 하나로 그룹지어서 한번에 파악하고 싶을때 Dispatch Group의 개념 사용. 즉, 그룹으로 묶인 작업의 마지막 시점을 파악

 

 


 

문제상황

아래는 비동기 함수를 두번 연속으로 호출하는 상황이다.

각각 0.2초씩, 0.4초씩 지연되도록 처리했다.

combineAsyncCalls 함수의 print 문이 찍혀야 되는데 안찍힌다.

이유는 fetchData 함수의 완료 핸들러가 실행 되기 전에 print 문이 호출되기 때문에 출력이 비어 있다 .

import Foundation

// 지연 후에 Int를 String으로 변환하는 함수
func fetchData(_ data: Int, delay: Double, completionHandler: @escaping (String)->()) {
    
    print("fetchData - 진입 ")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        
        print("fetchData - data  \(data) ")
        
        completionHandler("\(data)")
    }
    
    print("fetchData - 종료 ")

}


// 두 개의 비동기 호출을 결합하는 함수
func combineAsyncCalls(completionHandler: @escaping (String)->()) {
    
    print("combineAsyncCalls - 진입 ")

    var text = ""
    fetchData(0, delay: 0.4) { text += $0 }
    fetchData(1, delay: 0.2) { text += $0 }

    print("combineAsyncCalls - text  \(text) ")

    completionHandler(text)
    
    print("combineAsyncCalls - 종료 ")
}

// fetchData 함수의 완료 핸들러가 실행 되기 전에 호출되기 때문에 출력이 비어 있다 .
combineAsyncCalls() {
    print("combineAsyncCalls - 클로저 메소드 진입 ")

    print($0) // 빈공백

    print("combineAsyncCalls - 클로저 메소드 종료 ")

}

/*
 combineAsyncCalls - 진입
 fetchData - 진입
 fetchData - 종료
 fetchData - 진입
 fetchData - 종료
 combineAsyncCalls - text
 combineAsyncCalls - 클로저 메소드 진입

 combineAsyncCalls - 클로저 메소드 종료
 combineAsyncCalls - 종료
 fetchData - data  1
 fetchData - data  0

 */

 

 

해결 - 1 

DispatchGroup 을 이용해서 fetchData 함수의 완료 callback 함수가 호출 다 된 후, combineAsyncCallsWithDispatchGroup 함수의 print 문이 찍힌다.

combineAsyncCallsWithDispatchGroup  함수를 실행하면 두 번째 fetchData 함수는 첫 번째보다 완료하는 데 시간이 덜 걸리기 때문에 출력은 "10"이 된다. 

 

참고로 DispatchGroup 은 비동기 호출 문제를 해결해 주지만 순서대로 해결해 주지는 안는다. 순서대로 해결될려면 "01" 이 출력되어야 한다.

 

import Foundation


// 지연 후에 Int를 String으로 변환하는 함수
func fetchData(_ data: Int, delay: Double, completionHandler: @escaping (String)->()) {
    
    print("fetchData - 진입 ")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        
        print("fetchData - data  \(data) ")
        
        completionHandler("\(data)")
    }
    
    print("fetchData - 종료 ")

}

func combineAsyncCallsWithDispatchGroup(completionHandler: @escaping (String)->()) {
    
    // 디스패치 그룹
    let group = DispatchGroup()
    var text = ""
    
    
    // 0.4초 걸림
    group.enter()
    fetchData(0, delay: 0.4) {
        text += $0
        group.leave()
    }
    
    
    // 0.2초 걸림
    group.enter()
    fetchData(1, delay: 0.2) {
        text += $0
        group.leave()
    }
    
    
    // 마지막에 호출
    group.notify(queue: .main) {
        completionHandler(text)
    }
}

combineAsyncCallsWithDispatchGroup() {
    print("combineAsyncCallsWithDispatchGroup - 클로저 메소드 진입 ")

    print($0)

    print("combineAsyncCallsWithDispatchGroup - 클로저 메소드 종료 ")
    
    exit(0)
}

/*
 fetchData - 진입
 fetchData - 종료
 fetchData - 진입
 fetchData - 종료
 fetchData - data  1
 fetchData - data  0
 combineAsyncCallsWithDispatchGroup - 클로저 메소드 진입
 10
 combineAsyncCallsWithDispatchGroup - 클로저 메소드 종료
 */

 

해결 - 2 

DispatchSemaphore 사용

combineAsyncCallsWithSemaphore 함수를 호출하면 함수 fetchData가 완료되는 데 걸리는 시간에 관계없이 print 출력이 항상 "01" 이 다. 즉, 비동기 함수를 직렬로, 순서대로 처리를 한다.

참고 : 메인 스레드에서 세마포어를 사용할 수 없다

 

import Foundation


// 지연 후에 Int를 String으로 변환하는 함수
func fetchData(_ data: Int, delay: Double, completionHandler: @escaping (String)->()) {
    
    print("fetchData - 진입 ")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        
        print("fetchData - data  \(data) ")
        
        completionHandler("\(data)")
    }
    
    print("fetchData - 종료 ")

}


func combineAsyncCallsWithSemaphore(completionHandler: @escaping (String)->()) {
    
    // 세마포어
    let semaphore = DispatchSemaphore(value: 0)
    var text = ""
    
    // 메인 스레드에서 세마포어를 사용할 수 없다
    DispatchQueue.global().async {
        fetchData(0, delay: 0.4) {
            text += $0
            semaphore.signal()
        }
        semaphore.wait() //첫번째 fetchData 함수가 완료될때까지 기다린다.
        
        fetchData(1, delay: 0.2) {
            text += $0
            semaphore.signal()
        }
        semaphore.wait() //두번째 fetchData 함수가 완료될때까지 기다린다.
     
        
        // 완료 함수
        completionHandler(text)
    
        
    }//global().async - end

}

/*
 이제 이 함수를 호출하면 함수 fetchData가 완료되는 데 걸리는 시간에 관계없이 출력이 항상 "01" 이 됩니다.
 참고 : 메인 스레드에서 세마포어를 사용할 수 없다
 */
combineAsyncCallsWithSemaphore() {
    print("combineAsynccombineAsyncCallsWithSemaphoreCallsWithDispatchGroup - 클로저 메소드 진입 ")
    print($0)
    print("combineAsyncCallsWithSemaphore - 클로저 메소드 종료 ")

    exit(0)
}


/*
 fetchData - 진입
 fetchData - 종료
 fetchData - data  0
 fetchData - 진입
 fetchData - 종료
 fetchData - data  1
 combineAsynccombineAsyncCallsWithSemaphoreCallsWithDispatchGroup - 클로저 메소드 진입
 01
 combineAsyncCallsWithSemaphore - 클로저 메소드 종료

 */

 

 

참고

https://sujinnaljin.medium.com/

https://betterprogramming.pub/synchronizing-async-code-with-dispatchgroup-dispatchsemaphore-de814e485e82