Groups & Semaphores

박성민·2021년 7월 7일
0

iOS

목록 보기
23/30
post-custom-banner

DispatchGroup

  • DispatchQueue들을 그룹으로 묶어서, 후행 클로저 (일이 끝난다음의 처리)를 할 수 있도록 합니다.
  • 어떠한 task가 있고, group의 일부로써 task를 추적하려고 한다면, group을 dispatch queue의 async 메서드의 인수로 제공할 수 있습니다.
let group = DispatchGroup()

someQueue.async(group: group) { ... your work ... }
someQueue.async(group: group) { ... more work .... }
someOtherQueue.async(group: group) { ... other work ... }

group.notify(queue: DispatchQueue.main) { [weak self] in
  self?.textLabel.text = "All jobs have completed"
}

위의 예제 코드에서 볼 수 있듯이 group들은 하나의 dispatch queue에 강하게 연결되어있지 않습니다. 하나의 group을 실행해야하는 task의 우선순위에 따라 여러 queue에 제출할 수 있습니다. DispatchGroups는 제출된 모든 작업이 끝나는대로 notify를 받을 수 있게 notify(queue:) 메서드를 제공합니다.

Note: notification 자체는 async입니다. 제출된 작업들이 아직 완료되기전에, notify를 호출한 후 더 많은 작업들을 제출할 수 있습니다.

  • notify 매서드는 dispatch queue를 parameter로 사용합니다. 모든 작업이 끝난 경우 제공된 closure가 지정된 dispatch queue에서 실행됩니다.

Synchronous waiting

  • 어떠한 이유에 의해서 group의 완료 notification을 async하게 응답할 수 없는 경우에는 wait 메서드를 대신 사용할 수 있습니다. 이것은 모든 작업이 완료될 때까지 현재 queue를 block하는 sync메서드 입니다. 따라서 task가 끝날때까지 기다리는 시간을 지정하는 optional parameter가 있습니다. 만약에 지정하지 않으면 완료될 때까지 무한정 대기합니다.
let group = DispatchGroup()

someQueue.async(group: group) { ... }
someQueue.async(group: group) { ... }
someOtherQueue.async(group: group) { ... }

if group.wait(timeout: .now() + 60) == .timedOut {
  print("The jobs didn’t finish in 60 seconds")
}

Note: 현재 thread를 block한다는 것을 잊지마세요. main queue에서 절대로 wait메서드를 호출하지 마세요

  • 위의 코드에서는 wait가 반환되기 전에 60초까지 기다립니다. timeout이 일어난 후에도 작업은 계속 실행됩니다.

Wrapping asynchronous methods

  • Dispatch queue는 기본적으로 dispatch group과 함께 작업하는 것을 알고 있고, 시스템에 작업완료 신호를 처리합니다. 이 경우 completed의 의미는 코드블럭이 이것의 코스를 실행했다는 것을 의미합니다. closure 안에서 async 메서드를 호출한다면 내부의 async 매서드가 끝나기 전에 closure가 먼저 종료될 것입니다.
  • 따라서 내부의 콜이 끝나기 전에는 완료되지 않았다는 것을 알려야합니다. 이러한 경우에는 DispatchGroup에서 제공하는 enter, leave 메서드를 호출할 수 있습니다.
  • 예시로 실행중인 task의 카운트로 생각해보세요. 매번 enter할 때 마다 1씩 증가하고 매번 leave할때 마다 1씩 감소합니다.
queue.dispatch(group: group) {
// count is 1
  group.enter()
// count is 2
  someAsyncMethod {
    defer { group.leave() }
    // Perform your work here,
    // count goes back to 1 once complete
	}
}

group.enter()를 통해 dispatch group에 실행중인 다른 코드블럭(group에 전체적인 완료상태에 카운팅 되어야하는)이 있다는 것을 알려줍니다. 물론 해당하는 group.leave()와 페어링해야합니다. 그렇지 않으면 완료신호를 받을 수 없습니다. 오류 상태에서도 leave를 호출해야하기 때문에, defer문을 사용하면 어떻게 closure의 종료하는지와 상관없이 group.leave()가 실행됩니다.

  • 이전 코드 샘플처럼 간단한 경우 직접 enter / leave 쌍을 호출할 수 있습니다. 그러나 dispatch group과 함께 async 메서드들을 자주 사용한다면 필요한 call을 잊지 않게 메서드를 wrapping해야 합니다.
func myAsyncAdd(
  lhs: Int,
  rhs: Int,
  completion: @escaping (Int) -> Void) {
  // Lots of cool code here
  completion(lhs + rhs)
}

func myAsyncAddForGroups(
  group: DispatchGroup,
  lhs: Int,
  rhs: Int,
  completion: @escaping (Int) -> Void) {
    group.enter()

    myAsyncAdd(first: first, second: second) { result in
      defer { group.leave() }
      completion(result)
    }
}

Semaphores

  • 공유 resource에 대해 액세스할 수 있는 thread의 수를 제한해야하는 경우가 있습니다.
    예를 들어 네트워크에서 데이터를 다운로드하는 경우에 한번에 다운로드 되는 횟수를 제한할 수 있습니다. Dispatch queue를 이용해 모든 다운로드가 완료된 시점을 알 수 있습니다. 그러나 가져오는 데이터가 매우 크고 처리하기에 리소스가 많이 들기 때문에 한번에 4번 다운로드만 허용하려고 합니다.
    DispatchSemaphore를 사용하면 이러한 케이스를 해결할 수 있습니다. resource를 사용하기 전에 sync함수인 wait메서드를 호출하면 resource를 사용할 수 있을 때까지 thread가 실행을 일시중지합니다. 아직 아무도 소유권을 요구하지 않았다면 바로 액세스가 가능합니다. 누군가가 소유권을 가지고 있다면 작업이 끝났다는 signal을 보낼 때까지 기다립니다.
  • Semaphore를 생성할 때 resource에 허용되는 동시 액세스 수를 지정합니다. 만약에 배타적인 액세스를 원한다면 1로 지정하면 됩니다.
let semaphore = DispatchSemaphore(value: 4)

for i in 1...10 { queue.async(group: group) {
	semaphore.wait() 
	defer { semaphore.signal() }

	print("Downloading image \(i)")

	// Simulate a network wait 
	Thread.sleep(forTimeInterval: 3)

	print("Downloaded image \(i)")
	}
}
  • Dispatch group에서 반드시 leave()를 호출했던 것처럼 resource 사용이 끝나면 signal()을 호출해야합니다. resource를 해제하지 않고 나갈 방법이 없기 때문에 defer 블록을 사용하는 것은 가장 좋은 방법입니다.

출처: Concurrency by Tutorials Multithreading in Swift with GCD and Operations by Scott Grosch

profile
iOS시작~
post-custom-banner

0개의 댓글