Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #25] 2023 / 05 / 30

오늘은 간단히 DispatchGroup, DispatchSemaphore의 사용법에 대해서 알아보려고 합니다.

 

 

DispatchGroup

간단히 DispatchGroup에 대해 설명하면,

비동기 작업이 끝날 때까지 기다린 후, 작업 완료된 시점에서 처리를 할 수 있는 기능입니다.

 

기본 사용법을 간단히 알아봅시다.

DispatchGroup은 enter와 leave로 한 쌍으로 구성이 돼있습니다.

enter로 그룹의 작업을 추가하고 leave를 통해서 작업을 제거할 수 있습니다.

그러므로 사용할 땐, 항상 두 쌍으로 존재해야 됩니다. 아니면 에러가 발행하게 돼요.

 

만약, 여기서 더 이상 남아 있는 작업이 없다면,

nofity의 completion handler를 통하여 완료된 작업들을 처리할 수 있습니다.

let dispatchGroup = DispatchGroup()

// 다운로드 작업
func download(_ data: String, completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        // 오래 걸리는 작업
        Thread.sleep(forTimeInterval: 1)
        print("다운로드 시작 \(data)")
        completion(data)
    }
}

// 다운받은 데이터를 전달받아 처리하는 작업
func responder(_ data: String, completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        // 짧게 걸리는 작업
        Thread.sleep(forTimeInterval: 0.1)
        print("이미지 처리 중 \(data)")
        completion(data)
    }
}

var text = ""

for i in 0..<3 {
    dispatchGroup.enter()  // 그룹에 진입 합니다.
    download("\(i) : ⭐️") { data in
        text += "\n" + data
        
        responder("\(i) : 🔥") { data in
            text += "\n" + data
            dispatchGroup.leave()  // 그룹의 작업 완료 시점
        }
    }
}

// 만약 더 이상 enter 메서드가 없다면 notify를 실행합니다. (그룹의 모든 작업이 완료되면)
dispatchGroup.notify(queue: .main) {
    print("==================")
    print("모든 작업 종료 \(text)")
}
다운로드 시작 1 : ⭐️
다운로드 시작 2 : ⭐️
다운로드 시작 0 : ⭐️
이미지 처리 중 2 : 🔥
이미지 처리 중 0 : 🔥
이미지 처리 중 1 : 🔥
==================
모든 작업 종료 
0 : ⭐️
1 : ⭐️
2 : ⭐️
2 : 🔥
0 : 🔥
1 : 🔥

 

위에도 설명했지만, enter와 leave를 잘못 사용하면 에러가 발생하게 됩니다.

그래서 새롭게 도입된 방법이 있다고 합니다.

 

간단히 async 메서드에서 group을 추가만 해주면, enter와 leave를 사용하지 않아도 그룹으로 묶여서 동작하게 됩니다.

엄청 간단하네요.

let workQueue = DispatchQueue(label: "WorkQueue", attributes: .concurrent)
let dispatchGroup = DispatchGroup()

// 다운로드 작업
func download(_ data: String, completion: @escaping (String) -> Void) {
    workQueue.async(group: dispatchGroup) {
        // 오래 걸리는 작업
        Thread.sleep(forTimeInterval: 1)
        print("다운로드 시작 \(data)")
        completion(data)
    }
}

// 다운받은 데이터를 전달받아 처리하는 작업
func responder(_ data: String, completion: @escaping (String) -> Void) {
    workQueue.async(group: dispatchGroup) {
        // 짧게 걸리는 작업
        Thread.sleep(forTimeInterval: 0.1)
        print("이미지 처리 중 \(data)")
        completion(data)
    }
}

var text = ""

for i in 0..<3 {
    download("\(i) : ⭐️") { data in
        text += "\n" + data
        
        responder("\(i) : 🔥") { data in
            text += "\n" + data
            
        }
    }
}

dispatchGroup.notify(queue: .main) {
    print("==================")
    print("모든 작업 종료 \(text)")
}
다운로드 시작 1 : ⭐️
다운로드 시작 0 : ⭐️
다운로드 시작 2 : ⭐️
이미지 처리 중 2 : 🔥
이미지 처리 중 0 : 🔥
이미지 처리 중 1 : 🔥
==================
모든 작업 종료 
1 : ⭐️
0 : ⭐️
2 : ⭐️
2 : 🔥
0 : 🔥
1 : 🔥

 

이렇게 완료된 시점에서 데이터를 처리를 해줄 수 있습니다.

 

그런데 위의 동작에서 다운로드1이 완료되고 바로 이미지 처리1 작업을 해주고 싶은데

작업이 비동기로 처리되다 보니 작업의 순서가 랜덤으로 나타나게 됩니다.

 

이렇게 여러개의 파일을 동시에 다운로드 하는 작업이 있고

다운로드한 파일들을 순서대로 처리할 때, 유용하게 사용 할 수 있는 DispatchSemaphore라는 녀석이 있습니다.

 

 

DispatchSemaphore

동시에 처리할 수 있는 쓰레드 또는 작업의 수를 제한하는 기능을 합니다.

 

DispatchSemaphore는 쓰레드의 수를 제어하는 기능이긴 한데

보통 이렇게 순서를 갖게 하려면 하나의 쓰레드에서만 작업을 하므로 동시성을 포기해야겠죠...

 

간단히 DispatchSemaphore에 대해 원리를 설명하자면

semaphore에서 value는 일종의 쓰레드의 갯수라고 생각하면 됩니다.

그리고 signal과 wait을 통해 작업의 수(쓰레드)를 관리하여 제어할 수 있습니다.

 

semaphore의 value가 0일 때, 쓰레드 블록(Thread Block)이 발생하고 작업이 완료 될 때까지 기다립니다.

 

signal 메서드는 value가 1 만큼 증가하며, 하나의 쓰레드에서 작업을 시작합니다.

그리고 wait 메서드는 value를 1 만큼 감소 시킵니다.

let semaphore = DispatchSemaphore(value: 0)
semaphore.signal()  // 세미포어 밸류 값을 증가 시킨다.
semaphore.wait()    // 세미포어 밸류 값을 감소 시킨다.

 

사용하기 편하게 다시 설명하자면

DispatchSemaphore의 value 값이 0이라고 가정했을 때,

 

signal은 작업이 완료되는 시점을 알리는 역할을 합니다.

다시 말해서, 작업이 끝나면 signal을 호출하여 다음 작업이 진행될 수 있도록 신호를 보냅니다.

 

wait signal의 작업이 완료될 때까지 대기하는 역할을 합니다.

즉, 현재 작업이 완료될 때까지 다음 작업이 진행되지 않도록 쓰레드 블록(Thread Block)합니다.

 

바로 사용방법을 봅시다.

let workQueue = DispatchQueue(label: "WorkQueue", attributes: .concurrent)
let dispatchGroup = DispatchGroup()
let semaphore = DispatchSemaphore(value: 0)

// 다운로드 작업
func download(_ data: String, completion: @escaping (String) -> Void) {
    workQueue.async(group: dispatchGroup) {
        // 오래 걸리는 작업
        Thread.sleep(forTimeInterval: 1)
        print("다운로드 시작 \(data)")
        completion(data)
    }
}

// 다운받은 데이터를 전달받아 처리하는 작업
func responder(_ data: String, completion: @escaping (String) -> Void) {
    workQueue.async(group: dispatchGroup) {
        // 짧게 걸리는 작업
        Thread.sleep(forTimeInterval: 0.1)
        print("이미지 처리 중 \(data)")
        completion(data)
    }
}

var text = ""


for i in 0..<3 {
    download("\(i) : ⭐️") { data in
        text += "\n" + data

        responder("\(i) : 🔥") { data in
            text += "\n" + data
            // responder의 작업이 완료되는 시점을 보고해주는 것이다. (하나의 쓰레드로 동작)
            semaphore.signal()
        }
    }
    // Thread block이 발생, 위의 이미지 처리(responder)까지 완료를 기다린 후, 다시 for문 첫 번째 줄로 올라간다.
    semaphore.wait()
}

dispatchGroup.notify(queue: .main) {
    print("==================")
    print("모든 작업 종료 \(text)")
}

 

다운로드 시작 0 : ⭐️
이미지 처리 중 0 : 🔥
다운로드 시작 1 : ⭐️
이미지 처리 중 1 : 🔥
다운로드 시작 2 : ⭐️
이미지 처리 중 2 : 🔥
==================
모든 작업 종료 
0 : ⭐️
0 : 🔥
1 : ⭐️
1 : 🔥
2 : ⭐️
2 : 🔥

 

DispatchSemaphore는 개발자가

signal, wait의 시점을 항상 고민을 해야되서 엄청 번거롭고 힘듭니다.

 

새로 나온 async/await을 통해서도 간단하게 만들 수 있더라고요.

 

 

async/await

여기서는 코드만 소개하겠습니다.

 

깔끔하네요 ㅎㅎㅎ

// 다운로드 작업
func download(_ data: String) async -> String {
    // 오래 걸리는 작업
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    print("다운로드 시작 \(data)")
    return data
}

// 다운받은 데이터를 전달받아 처리하는 작업
func responder(_ data: String) async -> String {
    // 짧게 걸리는 작업
    try? await Task.sleep(nanoseconds: 1_000_000_00)
    print("이미지 처리 중 \(data)")
    return data
}


func runTask() async {
    var text = ""

    for i in 0..<3 {
        let download = await download("\(i) : ⭐️")
        text += "\n" + download

        let responder = await responder("\(i) : 🔥")
        text += "\n" + responder
    }
    
    print("==================")
    print("모든 작업 종료 \(text)")
}

Task {
    await runTask()
}
다운로드 시작 0 : ⭐️
이미지 처리 중 0 : 🔥
다운로드 시작 1 : ⭐️
이미지 처리 중 1 : 🔥
다운로드 시작 2 : ⭐️
이미지 처리 중 2 : 🔥
==================
모든 작업 종료 
0 : ⭐️
0 : 🔥
1 : ⭐️
1 : 🔥
2 : ⭐️
2 : 🔥

 

 

 

 

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!