본문 바로가기

iOS

[iOS] GCD 활용하기 2편 (DispatchGroup, DispatchSemaphore)

동시에 실행되는 작업들을 관리하는 방법에 대해서 알아보겠습니다. 여러 작업을 하나의 그룹으로 관리하고 하나의 리소스에 대한 접근을 통제해야 하는 문제들을 GCD를 활용하여 해결해보겠습니다.

DispatchGroup

DispatchGroup 은 디스패치 큐에 추가된 작업을 가상의 그룹으로 관리합니다. 서로 다른 디스패치 큐에 추가된 작업을 동일한 그룹에 추가하는 것도 가능합니다. 여러 작업을 하나의 작업으로 묶는 것이라고 생각하면 편합니다. 그러므로 그룹에 포함된 모든 작업이 완료되어야 그룹이 완료됩니다.

예시를 통하여 그룹이 어떻게 사용되는지 알아보겠습니다.

 

let queue1 = DispatchQueue(label: "ConcurrentQueue", attributes: .concurrent)
let queue2 = DispatchQueue(label: "SerialQueue")
let group = DispatchGroup()

 

작업을 추가할 큐를 두 개 만들고 해당 작업들을 관리할 그룹(group)을 선언하였습니다.

그룹에 추가하는 방법은 큐에 작업을 추가할 때 파라미터로 group 을 같이 넘겨주면 됩니다.

 

queue1.async(group: group) {
    for _ in 0..<10 {
        print("🐧", separator: "", terminator: "")
        Thread.sleep(forTimeInterval: 0.1)
    }
}

queue1.async(group: group) {
    for _ in 0..<10 {
        print("🐦", separator: "", terminator: "")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

queue2.async(group: group) {
    for _ in 0..<10 {
        print("🐤", separator: "", terminator: "")
        Thread.sleep(forTimeInterval: 0.3)
    }
}

// 그룹이 완료됨을 알리는 메소드
group.notify(queue: DispatchQueue.main) {
    print(" => 30 Brids!")
}

// 🐧🐦🐤🐧🐦🐧🐤🐧🐦🐧🐧🐧🐦🐤🐧🐦🐧🐤🐧🐦🐦🐤🐦🐤🐦🐦🐤🐤🐤🐤 => 30 Brids!

 

2개의 큐에 추가된 작업이 하나의 그룹으로 관리되기 때문에 모든 작업이 끝나고나서 결과(=>30 Birds!)가 뒤따라 나오게 됩니다.

DispatchSemaphore

DispatchSemaphore 는 기존의 카운팅 세마포어를 사용하여 다수의 작업이 하나의 리소스에 접근하는 것을 통제하는 객체입니다. 간단하게 waitsignal 을 이용하여 리소스 접근을 통제합니다. 이 기능을 이용하여 작업의 순서를 통제하는데에 사용될 수도 있습니다.

세마포어의 구현 패턴은 요청과 대기의 반복으로 볼 수 있습니다. 리소스 접근을 통제해야하는 지점에서 세마포어의 counting value 를 조정하며 작업을 컨트롤합니다.

  • wait(): 카운팅 값이 0보다 크다면 1을 감소시키고 실행을 허가합니다. 카운팅 값이 0이면 값이 증가할 때까지 대기합니다.
  • signal(): 카운팅 값을 1 증가시킵니다.

예시를 통하여 활용 방법을 알아보겠습니다.

 

let queue = DispatchQueue(label: "Queue", attributes: .concurrent)
let group = DispatchGroup()
var result: Int = 0

// 보통 리소스 풀을 관리할 때는 0보다 큰 값을 value 파라미터로 전달합니다.
// 여러 작업을 하나씩 순서대로 실행해야 한다면 1을 전달합니다.
let semaphore = DispatchSemaphore(value: 1)

queue.async(group: group) {
    for _ in 1...1000 {
        semaphore.wait() // 1 감소
        result += 1
        semaphore.signal() // 1 증가
    }
}

queue.async(group: group) {
    for _ in 1...1000 {
        semaphore.wait()
        result += 1
        semaphore.signal()
    }
}

queue.async(group: group) {
    for _ in 1...1000 {
        semaphore.wait()
        result += 1
        semaphore.signal()
    }
}

group.notify(queue: DispatchQueue.main) {
    print(result)
}

// 3000

 

위의 예시에서 세마포어를 사용하지 않았다면 결과가 3000 으로 나오지 않았을 것입니다. 같은 result 값에 동시에 접근하여 연산하려고 하기 때문에 3000보다 작은 값이 결과로 출력되었을 것입니다.

 

wait 사용시 타임아웃을 줄 수도 있습니다.

 

semaphore.wait(timeout: .now() + 10)

 

무한정 대기하는 것을 막기 위해 signal 을 꼭 호출해야 한다는 것을 주의해야 합니다.

정리

  • 큐에 추가된 작업들을 하나의 그룹으로 관리할 수 있습니다.
  • 그룹에 추가된 작업이 모두 완료되어야 그룹이 완료됩니다.
  • 세마포어를 활용하여 리소스 접근을 통제할 수 있습니다.
  • 작업의 순서를 지정해 주고 싶을 때 세마포어를 활용할 수도 있습니다.