ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동시성 프로그래밍(4) DispatchGroup, DispatchWorkItem, DispatchSemaphore
    iOS 2022. 2. 14. 22:56

    안녕하세요.

     

    저번 시간까지 DispatchQueue에 대해서 알아보았습니다.

     

    GCD = DispatchQueue라고 생각하시는 분들이 많은 거 같아요.

     

    하지만 DispatchQueue는 Dispatch(GCD) 프레임워크의 하위 클래스 중 하나입니다.

    Dispatch 프레임워크에는 이외에도 많은 클래스가 있는데요.

     

    오늘은 그중에서 DispatchGroup, DispatchWorkItem, DispatchSemaphore에 대해서 알아보겠습니다.


    1. DispatchGroup

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

     

    Apple Developer Documentation

     

    developer.apple.com

     

    작업들을 DispatchQueue에 넣으면 알아서 적절히 스레드로 보내 처리한다는 사실은 알았습니다.

     

    이제 여러 스레드로 보내진 작업들이 언제 끝나는지 알고 싶어 졌습니다.

     

    이럴 경우 사용하는 것이 DispatchGroup입니다.

     

    DispatchGroup은 관찰하고 싶은 작업들을 하나의 그룹으로 묶어서 단일 개체로 볼 수 있게 해 줍니다.

    위의 작업들을 그룹으로 묶음으로써 그룹의 모든 작업이 완료되는 시점을 알 수 있게 되는 것입니다.

     

    예를 들어서 여러 애니메이션 효과가 겹쳐있을 경우 애니메이션이 모두 종료된 시점을 알고 싶거나,

    이미지를 여러 장 다운로드할 경우 이미지가 모두 다운로드된 시점을 알고 싶은 경우 사용할 수 있습니다.

     

    사용방법은 간단합니다.

    let group = DispatchGroup() // 그룹 생성하기
    
    DispatchQueue.global().async(group: group) { // 어떤 그룹에 넣을 것인지 결정
        print("first")
    }
    
    DispatchQueue.global(qos: .utility).async(group: group) { // 같은 그룹이라고 동일한 큐에서 실행시킬 필요는 없다.
        sleep(2)
        print("second")
    }
    
    group.notify(queue: DispatchQueue.main) { // 모든 작업이 완료될 시 실행될 작업을 실행할 큐를 지정
        print("complete")
    }
    
    //실행결과
    //first
    //second
    //complete

    DispatchGroup을 생성하고,

    작업을 Group에 넣어주고

    notify 메서드를 통해 작업이 모두 완료될 경우 실행할 작업을 정의합니다.

     

    notify가 아닌 wait 메서드도 존재하는데요.

    group.wait()

     

    notify는 비동기적으로 동작하지만 wait는 동기적으로 동작합니다.

    wait는 모든 작업이 완료될 때까지 현재 대기열을 Block 합니다.

    그러니깐 main Queue에서는 wait를 사용해서는 안됩니다.

     

    wait 메서드에는 영원히 대기열을 Block하지 않게 하기 위하여

    timeout 파라미터를 통해 대기할 시간을 지정할 수 있습니다.

    if group.wait(timeout: .now() + 60) == .timeOut {
    	print("작업이 60초 안에 종료하지 않았습니다.")
    }

    DispatchGroup 사용 시 클로저 내에서 비동기 함수를 호출할 때를 생각해봅시다.

    let group = DispatchGroup()
    
    DispatchQueue.global().async(group: group) {
        print("start")
        //비동기 이미지 다운로드
        URLSession.shared.downloadTask(with: url) { url, response, error in
        	// do something
        }.resume()
        print("finish")
    }
    
    group.notify(queue: DispatchQueue.main) {
    	print("complete")
    }
    
    // 설명을 위해 작성된 코드입니다.

    URLSession.shared.downloadTask는 비동기로 동작하는 코드입니다.

    따라서 즉시 반환됩니다.

     

    그림을 통해 살펴보겠습니다.

    실제로 알고 싶은 시점은 이미지가 다운로드 완료된 시점인데

    비동기 코드는 즉시 반환되기 때문에 원하는 시점을 알 수 없습니다.

     

    이럴 경우 추가적인 코드를 작성해야 합니다.

    바로 enter()와 leave()입니다.

    let group = DispatchGroup()
    
    DispatchQueue.global().async(group: group) {
        print("start")
        //비동기 이미지 다운로드
        group.enter() // task + 1, 작업 시작합니다~
        URLSession.shared.downloadTask(with: url) { url, response, error in
        	// do something
            group.leave() // task - 1, 작업 끝났습니다~
        }.resume()
        print("finish")
    }
    
    group.notify(queue: DispatchQueue.main) {
    	print("complete")
    }
    
    // 설명을 위해 작성된 코드입니다.

    enter()는 작업이 그룹에 들어갔음을 알립니다.

    leave()는 작업이 그룹에 들어간 작업이 실행을 완료했음을 알립니다.

     

    그룹에 있는 모든 작업이 완료되면 notify() 메서드가 실행됩니다.

     

    이미지 다운로드가 완료된 시점에 leave()를 해줌으로 원하는 시점에 작업이 완료되었음을 알릴 수 있습니다.


    2. DispatchWorkItem

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

     

    Apple Developer Documentation

     

    developer.apple.com

    그동안은 작업을 클로저 형태로 Queue에 바로 보냈었습니다.

    DispatchQueue.global().async {
        print("I am a task")
    }

     

     

    DispatchWorkItem은 작업을 캡슐화해서 미리 정의해 놓은 객체입니다.

    let item1 = DispatchWorkItem {
        print("I am a workItem1")
    }
    
    let item2 = DispatchWorkItem(qos: .utility) {
        print("I am a workItem2")
    }
    
    item1.perform()
    DispatchQueue.global().async(execute: item2)

     

    위와 같이 작업을 미리 정의하고 Queue에 보낼 수 있습니다.

     

    perform() 메서드를 통해서 현재 스레드에서 작업을 실행할 수도 있고, async(execute: ) 메서드를 통해서 비동기적으로 작업을 실행할 수도 있습니다.

     

    DispatchWorkItem을 사용하면 그냥 클로저를 보내는 것에 비해 두 가지 장점이 존재합니다.

     

    바로 cancel() 메서드와 notify() 메서드를 사용할 수 있습니다.

     

    cancel() 메서드의 경우,

    작업이 아직 시작되지 않은 경우에는 작업을 제거합니다.

    let item = DispatchWorkItem {
        print("I am a workItem")
    }
    
    item.cancel()
    
    DispatchQueue.global().async(execute: item) // 출력되지 않음

     

    그러나 시작된 경우 작업을 멈추는 것이 아니라, DispatchWorkItem의 isCancelled 프로퍼티를 true로 설정합니다.

    let item = DispatchWorkItem {
        print("I am a workItem")
    }
    
    DispatchQueue.global().async(execute: item)
            
    print(item.isCancelled)
            
    item.cancel()
            
    print(item.isCancelled)

    이렇게 말이죠.

     

    notify() 메서드는 작업 이후에 실행될 작업을 지정할 수 있습니다.

    let item = DispatchWorkItem {
        print("I am a workItem")
    }
            
    let item2 = DispatchWorkItem {
        print("I am a second workItem")
    }
            
    item.notify(queue: DispatchQueue.main, execute: item2)
            
    item2.notify(queue: DispatchQueue.main) {
        print("I am last task")
    }
    
    DispatchQueue.global().async(execute: item)

     

    이렇게 DispatchWorkItem을 쓰면 약하게나마 작업을 중지하고, 순서를 지정할 수 있습니다.

    하지만 이러한 기능을 제대로 사용하고 싶다면 Operation을 사용하는 것이 더 낫습니다.


    3. DispatchSemaphore

     

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

     

    Apple Developer Documentation

     

    developer.apple.com

     

    Dispatch는 Semaphore 또한 제공합니다.

    Semaphore는 공유 자원에 접근하는 Thread를 제한할 필요가 있는 경우 사용됩니다.

     

    사용법은 매우 간단합니다.

    let semaphore = DispatchSemaphore(value: 3) // 접근 가능한 작업 초기 값 설정

    위와 같이 접근 가능한 작업의 초기값을 설정하고 Semaphore를 생성합니다.

    초기값은 0 이상이어야 합니다.

     

    공유 자원에 접근할 때는 wait()를 사용하고 나올 때는 signal()을 사용합니다.

     

    wait() 메서드를 사용하게 되면 value의 값이 1 감소하고 signal()을 사용하면 value가 1 증가합니다.

     

    value의 값이 0보다 작게 되게 되면 공유 자원에 접근을 막는 것이죠.

    let semaphore = DispatchSemaphore(value: 3)
            
    for i in 1...6 {
        semaphore.wait() // value - 1
        DispatchQueue.global().async {
            print("\(i)번째 작업 시작")
            sleep(3)
            print("\(i)번째 작업 끝")
            semaphore.signal() // value + 1
            }
    }

    6개의 작업을 비동기적으로 실행시켜도 DispatchSemaphore를 사용한다면 한 번에 3개의 작업만을 실행시킬 것입니다.

    여기서 알아두셔야 할 점이 있습니다.

     

    DispatchSemaphore 생성 시 설정하는 value가 초기값이라는 사실입니다.

     

    signal() 메서드를 통해 value를 초기값보다 크게 만드는 것도 가능합니다.

     

    따라서 공식문서에서는 초기값으로 0을 주는 방법 또한 설명하고 있습니다.

     

    두 개의 스레드를 동기화하기 위해서 초기값에 0을 줄 수 있습니다.

     

    두 개의 스레드 중 선행되어야 할 작업이 있을 경우 선행될 작업이 완료된 후 후행될 작업을 실행하는 것이죠.

     let semaphore = DispatchSemaphore(value: 0) // value = 0
     
     DispatchQueue.global().async {
         print("먼저 실행되어야 할 작업")
         semaphore.signal() // value += 1
     }
     
     DispatchQueue.global(qos: .userInteractive).async {
         semaphore.wait() // value -= 0
         print("나중에 실행되어야 할 작업")
     }

    초기값을 0으로 준 경우 wait() 메서드 이후의 작업은 signal()이 호출되기 전에는 실행되지 않을 것입니다.

    0에서 wait()를 실행할 경우 음수가 되기 때문입니다.

     

     

     

    위와 같이 서로 다른 스레드에서 이루어지는 작업을 동기화할 수 있습니다.


    오늘은 DispatchGroup, DispatchWorkItem, DispatchSemaphore에 대해서 알아보았습니다.

    DispatchQueue뿐만 아니라 위의 class들도 적절하게 사용한다면 앱의 최적화에 큰 도움이 될 거라 생각합니다.

     

    Ref.

    https://www.inflearn.com/course/iOS-Concurrency-GCD-Operation/dashboard

     

    iOS Concurrency(동시성) 프로그래밍, 동기 비동기 처리 그리고 GCD/Operation - 디스패치큐와 오퍼레이션

    동시성(Concurrency)프로그래밍 - iOS프로그래밍에서 필요한 동기, 비동기의 개념 및 그를 확장한 GCD 및 Operation에 관한 모든 내용을 다룹니다., - 강의 소개 | 인프런...

    www.inflearn.com

     

     

     

     

     

Designed by Tistory.