관리 메뉴

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

[iOS - swift] 1. Concurrent Programming - NSLock, DispatchSemaphore 사용 방법 본문

iOS 응용 (swift)

[iOS - swift] 1. Concurrent Programming - NSLock, DispatchSemaphore 사용 방법

jake-kim 2022. 8. 11. 22:01

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로 동적으로 작업 추가, 취소하는 모듈 구현방법

NSLock 개념

(자세한 ThreadSafe개념과 NSLock 개념은 이곳 참고)

  • NSLock은 이름에서 알 수 있듯이, Objective-C에서부터 만들어졌고 Thread safe하게 관리해주는 역할
    • NSLock의 가장 단점은 NSLock을 해제할 때 같은 스레드에서만 해제해야 잠금이 해재됨
      (애플 공식 문서에서도 빨갛게 Warning으로 적혀있는 부분)

https://developer.apple.com/documentation/foundation/nslock

  • NSLock 사용방법
let lock = NSLock()

lock.lock()
// this is critical section
// processing some task...
lock.unlock()
  • NSLock을 잘못쓰는 경우1
    • lock()을 호출한 곳이 global()안의 스레드지만, unlock()을 한곳이 main이므로 unlock동작이 안됨
    • NSLock은 반드시 lock()을 호출한 스레드에서 unlock()을 호출해야 동작
func runWrongFirstNSLock() {
  let lock = NSLock()
  DispatchQueue.global().async {
    lock.lock()
  }
  
  print("wait for task A")
  DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    print("finish Task A")
    lock.unlock()
  }
  
  print("Call after task A is done")
}
  • NSLock을 잘못쓰는 경우2
    • global()로 선언해서 같은 큐이지만, DispatchQueue에는 여러개의 쓰레드가 동작하므로 lock할때와 unlock할때의 쓰레드가 다를 수 있음
func runWrongSecondNSLock() {
  let lock = NSLock()
  DispatchQueue.global().async {
    lock.lock()
  }
  
  print("wait for task A")
  DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
    print("finish Task A")
    lock.unlock()
  }
  
  print("Call after task A is done")
}

DispatchSemaphore 개념

(자세한 DispatchSemaphore 개념은 이곳 참고)

  • 세마포어를 관리하는 API이며, 세마포어 값을 초기화하고, wait()와 signal()을 이용하여 스레드 작업 관리 제어
    • wait(): 자원 점유 (-1)
    • signal(): 자원 종료 (+1)
let semaphore = DispatchSemaphore(value: 2)

semaphore.wait()
// 스레드 2개 진입 가능
// processing some task...
semaphore.signal()
  • NSLock과의 차이점
    • NSLock은 lock을 수행한 스레드에서 unlock을 수행해야 적용
    • DispatchSemaphore는 wait()를 수행한 스레드가 아닌, 다른 스레드에서 signal()을 수행해도 적용
  • DispatchSemaphore의 기능 2가지
    1. 세마포어의 값을 0보다 크게 설정 - 동시에 접근할 수 있는 스레드의 갯수 지정 기능
    2. 세마포어의 값을 0으로 설정 - blocking 기능 (해당 작업이 끝날때까지 기다리고 나서 작업을 수행시키고 싶은 경우)
  • 세마포어에서의 핵심은 wait()를 호출하는 스레드가 중요 (메인 스레드에서 호출하면 UI가 멈춘상태가 되므로 주의)

세마포어의 값을 0보다 크게 한 경우 예시)

  • 세마포어 value를 2로 설정하여, 스레드가 2개씩 들어가면서 forEach문 내부에 1초마다 2개씩 작업
func runTwoSemaphore() {
  let semaphore = DispatchSemaphore(value: 2)
  
  (1...10).forEach { i in
    DispatchQueue.global().async() {
      semaphore.wait() //semaphore 감소
      print("Entry critcal section \(i)")
      sleep(1)
      print("Exit critcal section \(i)")
      semaphore.signal()
    }
  }
}

세마포어의 값을 0으로 한 경우 예시 - block기능)

  • 잘못 사용한 경우1
    • 세마포어는 main thread에서 부르게 되면 UI도 같이 동작 wait하므로 MainThread에서 wait 금지
    • 이 메소드를 viewDidLoad에서 호출하게되면 main thread로 인식하고 아래 semaphore.wait()도 메인 스레드이므로 메인스레드가 block되면서 UI가 그려지지 않는 현상이 존재
func runWrongFirstZeroSemaphore() {
  let semaphore = DispatchSemaphore(value: 0)
  
  print("wait for task A")
  DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
    print("finish Task A")
    semaphore.signal()
  }
  
  // semaphore가 0이라 task A 종료까지 block
  semaphore.wait()
  print("Call after task A is done")
}

메인스레드에서 wait()하여, signal이 호출된 후에 button이 그려지는 버그가 존재

  • 잘 사용한 경우1
    • DispatchQueue.global()를 사용하면 안에서 스레드를 새로 생성하므로, 다른 global queue의 스레드와 겹치지 않게 되어, wait()되어도 다른곳에서 global로 접근해도 block되지 않고 사용이 가능
func runGoodZeroSemaphore() {
  let semaphore = DispatchSemaphore(value: 0)
  
  print("wait for task A")
  DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
    print("finish Task A")
    semaphore.signal()
  }
  
  // semaphore가 0이라 task A 종료까지 block
  DispatchQueue.global().async {
    semaphore.wait()
    print("Call after task A is done")
  }
  
  DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    print("Test - can call before task A done")
  }
}

/*
wait for task A
Test - can call before task A done
finish Task A
Call after task A is done
*/
  • 잘 사용한 경우2
    • DispatchQueue.global()로 주지 않고 아예 별도의 스레드 안에서 wait()하게 하여 해당 스레드만 wait가 적용되게끔 하는 방법
func runBestBlockSemaphoreUsingThread() {
  let semaphore = DispatchSemaphore(value: 0)
  let thread = Thread {
    print("wait for task A")
    
    // wait 하는 곳의 스레드가 wait되므로 Thread로 따로 빼야 효율적
    // 만약 DispatchQueue.global()에서 wait하면 global()에 사용되는 스레드들이 wait되어서 비효율적
    semaphore.wait()
    print("Call after task A is done")
  }
  
  Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
    print("finish Task A")
    semaphore.signal()
  }
  
  thread.start()
  
  // semaphore와 연관없는 global() 큐
  DispatchQueue.global().async {
    print("바로 실행되어야함")
  }
  
  DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
    thread.cancel()
  }
}

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

 

* 참고

https://developer.apple.com/documentation/foundation/nslock

Comments