관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. Concurrent Programming - DispatchSemaphore로 코루틴의 CompletableDeferred 구현방법 (async, await 구현방법) 본문

iOS 응용 (swift)

[iOS - swift] 2. Concurrent Programming - DispatchSemaphore로 코루틴의 CompletableDeferred 구현방법 (async, await 구현방법)

jake-kim 2022. 8. 12. 23:18

1. Concurrent Programming - NSLock, DispatchSemaphore 사용 방법

2. Concurrent Programming - DispatchSemaphore로 코틀린의 CompletableDeferred 구현방법

3. Concurrent Programming - DispatchQueue의 serial, concurrent, async, sync 이해하고 사용하기

4. Concurrent Programming - Thread Safe Array 구현방법 (DispatchQueue의 barrier 사용)

5. Concurrent Programming - OperationQueue로 동적으로 작업 추가, 취소하는 모듈 구현방법

CompletableDeferred

  • 코틀린의 코루틴에 있는 기능이고, 이 곳에 추가해놓고 완료되기까지 block
  • 아래와 같은 기능의 메소드가 존재 (기능 설명을 위해 임시로 정한 메소드)
    • complete(): 작업이 완료된 경우 호출 (block하고 있는 곳에서 block 해제)
    • remove(): 작업 삭제 (block하고 있는 곳에서 block 해제)
    • await(timeout:): 타임아웃을 정하고 complete()나 remove()가 불릴때까지 block하고 있는 기능
  • concurrent하도록 쓸수 있지만, swift에서 구현할때 DispatchSemaphore를 적용하여 block되는 기능을 적용할 것이므로 하나의 스레드씩만 접근 가능하게 설계

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-completable-deferred/

기본 지식) iOS의 Thread 관리

  • main thread는 오직 한 개 존재
  • 그 밖의 thread는 여러개 존재
  • 별도의 스레드를 선언하지 않거나 DispatchQueue.global()과 같이 선언하지 않고 코드를 선언하면 모두 main thread에서 수행
    • a 함수에서 b 함수를 호출하면, b의 스레드는 a의 스레드와 동일

ex) 스레드 관리 - 호출한 곳과, 호출 당한곳의 스레드 확인

  • testThread안의 스레드는 호출한 곳과 동일한 main thread
// 스레드의 정보를 보기 위한 extension
extension Thread {
  class func printCurrent() {
    print("\(Thread.current), isMainThread? = \(Thread.isMainThread)")
  }
}

class ViewController: UIViewController {
  let completableDeferredSet = CompletableDeferredSet<Int>()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    Thread.printCurrent() // <_NSMainThread: 0x6000008f4680>{number = 1, name = main}, isMainThread? = true
  }
  
  func testThread() {
    Thread.printCurrent() // <_NSMainThread: 0x6000008f4680>{number = 1, name = main}, isMainThread? = true
  }
}
  • 메인스레드는 오직 한개이므로, DispatchQueue.main.async로 감싼 후 메소드를 호출해도, 메소드 안의 스레드는 동일
class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    Thread.printCurrent() // <_NSMainThread: 0x600001c687c0>{number = 1, name = main}, isMainThread? = true
    DispatchQueue.main.async {
      self.testThread()
    }
  }
  
  func testThread() {
    Thread.printCurrent() // <_NSMainThread: 0x600001c687c0>{number = 1, name = main}, isMainThread? = true
  }
}

CompletableDeferredSet 구현 방법

  • NSLock도 스레드 관리를 할 수 있지만, NSLock은 같은 스레드에서만 lock을 풀수 있고, 다른 스레드에서도 block을 자유롭게 풀 수 있어야 하므로 DispatchSemaphore를 사용
  • wait하여 A 스레드에 block을 걸고, B 스레드에서 complete()를 호출했을때 A스레드 안의 lock이 풀리면서 A 스레드 안에 남아있는 작업을 처리하는 기능을 구현
    • (pub-sub 패턴에서 사용하는 방법)
  • subscriber 하는쪽과 Publisher 하는쪽의 스레드가 다른 경우, subscriber에서 block을 걸어놓고, publisher에서 작업 완료 시 block을 풀어주면, Subscriber쪽 잔여 작업을 수행하는 것

ex) completableDeferredSet 구현 전에 사용하는쪽 먼저 보기

1. 메인 스레드에서 block이 되면 안되므로, Thread 하나를 만들어서 작업을 insert 하고난 후, await() 수행
(block되어 "2.대기 종료" 코드 라인이 실행 안되는 상태)

2. 다른 스레드에서 3초 후 작업이 끝나면, complete()를 보내어, 선언했던 스레드 안에 block이 풀리면서 "2.대기 종료"가 출력

class ViewController: UIViewController {
  let completableDeferredSet = CompletableDeferredSet<Int>()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // 1
    Thread {
      self.completableDeferredSet.insert(1)
      self.completableDeferredSet.await()
      print("2.대기 종료!")
    }
    .start()
    
    // 2
    DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
      print("1.작업 종료")
      self.completableDeferredSet.complete()
    }
  }
}

/*
여기 동작?
1.작업 종료
2.대기 종료!
*/
  • 구현 아이디어
    • 안에 DispatchSemaphore(value: 0)을 넣어서 block 기능을 사용 (이 개념은 이전 포스팅 글 참고)
  • 클래스와 필요한 프로퍼티 선언
    • workSet: 작업들을 저장
    • canSignal: DispatchSemaphore의 value값을 0과 1로로 고정시키기 위한 값
final class CompletableDeferredSet<T: Hashable> {
  private var workSet = Set<T>()
  // DispatchSemaphore로 부터 value값을 얻을 수 없기 때문에 별도의 프로핕 선언하여 관리
  private var canSignal = false
  private let semaphore = DispatchSemaphore(value: 0)
}
  • 구현할 메소드
    • complete(): 작업이 끝난 경우, set에 있는 작업을 제거하고 signal을 보내는 용도
    • insert(): set에 작업을 추가하고, signal이 필요하면 signal을 보내는 용도
    • remove(): set에 작업을 제거하고, signal이 필요하면 signal을 보내는 용도
    • await(timeoutSeconds:): 작업을 wait하는 것
  func complete() {
    self.signalSemaphoreIfNeeded()
  }
  
  func insert(_ job: T) {
    self.signalSemaphoreIfNeeded()
    self.workSet.insert(job)
  }
  
  func remove(_ job: T) {
    self.signalSemaphoreIfNeeded()
    self.workSet.remove(job)
  }
  
  func await() {
    self.waitSemaphoreIfNeeded()
  }
  
  private func signalSemaphoreIfNeeded() {
    guard self.canSignal else { return }
    self.canSignal.toggle()
    self.semaphore.signal()
  }
  
  private func waitSemaphoreIfNeeded() {
    guard !self.canSignal else { return }
    self.canSignal.toggle()
    self.semaphore.wait()
  }
  • 빌드해보면 block되어 원하는 형태로 실행된것을 확인
class ViewController: UIViewController {
  let completableDeferredSet = CompletableDeferredSet<Int>()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    Thread {
      self.completableDeferredSet.insert(1)
      self.completableDeferredSet.await()
      print("2.대기 종료!")
    }
    .start()
    
    DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
      print("1.작업 종료")
      self.completableDeferredSet.complete()
    }
  }
}

/*
여기 동작?
1.작업 종료
2.대기 종료!
*/

await에 타임아웃 넣기

  • await를 사용할 경우, 계속 기다리는 경우가 존재하는데 이를 방지하기 위해서 타임아웃을 사용
  • DispatchSemaphore의 wait(timeout:)을 사용하면 리턴값으로 DispatchTimeoutResult를 받는데, 이 값을 사용하여 타임아웃이 발생했는지 확인이 가능

https://developer.apple.com/documentation/dispatch/dispatchsemaphore/1780618-wait

  • DispatchTimeoutResult는 block 되어있다가 작업 가능할때 리턴되므로 바로 사용이 가능
    • DispatchTimeoutResult는 success타입과 timedOut타입이 존재하여 timedOut이 발생했을때 처리만 해주면 손쉽게 처리가 가능

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

  private func waitSemaphoreIfNeeded(timeoutSeconds: TimeInterval) {
    guard !self.canSignal else { return }
    self.canSignal.toggle()
    guard case .timedOut = self.semaphore.wait(timeout: .now() + timeoutSeconds) else { return }
    self.canSignal.toggle()
  }
  • 사용하는쪽
  override func viewDidLoad() {
    super.viewDidLoad()
    
    Thread {
      self.completableDeferredSet.insert(1)
      self.completableDeferredSet.await(timeoutSeconds: 3.3) // <-
      print("2.대기 종료!")
    }
    .start()
    
    DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
      print("1.작업 종료")
      self.completableDeferredSet.complete()
    }
  }

* 전체 코드: https://github.com/JK0369/ExCompletableDeferred

* 참고

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

https://developer.apple.com/documentation/dispatch/dispatchsemaphore/1780618-wait

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-completable-deferred/

 

Comments