Youngminah/TIL

GCD와 Operation Queue, 동기와 비동기, 직렬과 동시

Youngminah opened this issue · 0 comments

image

아래의 Main.sync의 데드락 발생 원인을 정확히 이해한다면 당신은 이미 GCD고수...





결론부터 미리보기 (이거 이해했으면 이 글 안읽어도됨 하지만 읽어줘...)

  • 우선 직렬, 동시의 명시로 들어온 작업을 순차적큐(메인큐) 로 보내던가 동시큐(글로벌큐) 로 보내던지 큐로 보내는걸 결정함.
  • 직렬큐 또는 동시큐로 보내면 항상 그 작업은 맨 뒤로 보내지게 됨
  • 그리고 작업을 보낸 현재의 그 스레드 !! 에서 보낸 작업이 끝날때 까지 다른 작업을 할것인지 안할것인지 (동기 비동기) 결정함.
  • 위의 박스친 말들이 제대로 나온 문서가 거~~~~~의 없다
  • 그러니까 헷갈릴수 밖에 없지..


애플에서의 동기, 비동기 큐

  • 쉽고 편한 멀티 스레딩 처리를 위해 애플은 세가지의 API를 제공
  • GCD(Grand Central Dispatch)라는 C기반의 저수준 API
  • NSOperation이라는 Obj-C 기반으로 만들어진 고수준 API
  • NSOperation은 GCD보다 약간의 오버헤드가 더 발생되고 느리지만
  • GCD에서는 직접 처리해야 하는 작업들을 지원 하고 있기 때문에 (KVO관찰, 작업취소 등등)
  • 그정도는 감수하고 (NSOperation를)사용할만하다.
  • 마지막으로 Swift 5.5에서 생긴 Async/await API
  • async/await는 순수한 swift 언어로 구성됨.
  • async/await는 따로 정리함 링크바로가기

GCD API

  • main queue : 메인 스레드(UI 스레드)에서 사용 되는 Serial Queue로 모든 UI 처리는 메인 스레드에서 처리❗️
DispatchQueue.main.sync {...} // 대부분에서 사용 불가능 뒤에서 설명
DispatchQueue.main.async {...}
  • global queue : Concurrent Queue임❗️
DispatchQueue.global().sync {}
DispatchQueue.global(qos: .background).async {}
  • Custom Dispatch Queue : Serial Queue, Concurrent Queue 정의 생성가능. default는 serial
Dispatch Queue(label: "com.serialQueue").async {}
Dispatch Queue(label: "com.concurrentQueue",
              qos: .default,
              attributes: .concurrent,
              autoreleaseFrequency: .inherit,
              target: nil).async {}

Operation Queue API

  • GCD와 비교했을땐 추가적인 오버해드가 있으나,
  • 다양한 작업들 가운데 의존성을 추가할 수 있고, 재사용, 취소, 중지시킬 수 있다.
  • Operation을 일시 중지, 다시 시작 및 취소를 할 수 있다.🖍
  • KVO를 사용할 수 있는 많은 프로퍼티들이 있다.
    isCancelled, isAsynchronous, isExecuting, isFinished, isReady, dependencies, queuePriority, completionBlock


동기 vs 비동기

동기 (Sync)

  • 스레드에서 보낸 작업이 끝날 때까지 현재 스레드는 다른 작업을 하지 않고 기다린다.
  • 해당 작업이 끝날 때 까지 스레드는 다른 작업 하지 않고 Block상태를 유지
  • 동기는 설계가 간단하고 직관적이지만,
  • 결과가 주어질 때 까지 대기를 해야하는 단점
  • 동기의 경우 하나의 작업이 Queue에서 빠져나갈 때까지 기다리기 때문에, Serial 이냐 Concurrent냐의 차이는 없다.

비동기 (Async)

  • 보낸 작업이 끝나는 것을 기다리지 않고 현재 스레드는 다른 작업을 시작.
  • 비동기는 설계는 복잡하지만
  • 자원을 효율적으로 사용할 수 있다는 장점
  • 동기 방식보다 대기시간을 줄여줄 수가 있어서 효율적인 면이 있다.
  • 병렬 처리와 스레드 풀에 기반을 둔 비동기 방식을 구현 (그래서 병렬이라기보단 비동기라고 얘기한다. 비동기가 더 큰 개념)


직렬 vs 동시

  • 위에서 비동기, 동기로 보낸 그 작업 들을 순차적❗️으로 처리할지 결정하는 것
  • 정확히는 직렬큐(순차처리큐) 또는 동시큐(순차고려x큐)의 맨뒤로 보내는 것

직렬 (Serial)

  • 단 하나의 쓰레드로만 작업을 보내는 대기열
  • GCD에서 mainQueue에서 mainthread로 보냄
  • 즉 순차적으로 처리됨

동시 (Concurrent)

  • 여러개의 다른 쓰레드로 작업을 보내는 대기열
  • GCD에서 globalQueue또는 customQueue에서 여러 스레드로 나눠 보냄
  • 순서는 상관없게됨


비동기(async)와 동시(concurrent)는 비슷해보이지만 아예 다른 개념❗️

  • 직렬, 동시는 큐로 들어온 작업들을 순차적으로 처리할지 순서 상관 없이 실행할지 결정. (순차 처리 or 순서 상관 x)
  • 동기, 비동기큐로 보내고 현재의 스레드는 다른 작업을 할 수 있게 할지 말지 결정. (다른 작업 가능 or 불가능)


직렬(Serial)을 사용하는 이유

  • 바로 작업의 '순서'가 중요할 때 직렬이 사용된다,
  • 동시는 여러개의 쓰레드에서 분산 작업하기 때문에 순서를 상관하지 않고 쓰레드에 할당된 작업은 각 쓰레드가 모조리 처리한다.
  • 따라서, 만약 어떤 작업이 꼭 먼저 이뤄져야 한다면, 그럴때는 직렬을 사용하는게 좋다.
  • 프로그램의 성능/반응성 을 올리고 최적화를 하기 위해서 하는 것이 동시성 프로그래밍


네트워크 작업

Alamofire 라이브러리

  • 네트워크를 처리할 때에는 보통 비동기로 처리한다.
  • Alamofire에서는 자동으로 비동기로 처리해준다.

라이브러리 사용을 안한다면?

DispatchQueue.global().async {
      if let url = URL(string: self.url), let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
          DispatchQueue.main.async {
              self.imageView.image = image
          }
      }
}
  • 비동기 처리 뒤, 메인쓰레드의 UI를 처리하는 것들은
  • 다시 코드로 DispatchQueue.main.async를 이용하여 메인쓰레드에서 처리할 수 있도록 한다.
  • 보통 비동기 처리를 하지 않았을 경우에는 메인쓰레드에서 처리하기때문에
  • 메인쓰레드 처리 코드는 넣지 않아도 된다.!!


main.sync가 오류가 나는 이유❗️

코드로 살펴보기

@IBAction func mainAsync(_ sender: UIButton) {
    print("HELLO WORLD") //1블럭

    for i in 1...100 {  
        print(i, terminator: " ")   //2블럭
    }
    
    DispatchQueue.main.sync {  // 3 블럭
        for i in 101...200 {
            print(i, terminator: " ") 
        }
    }
    print("\nBYE BYE WORLD") // 4블럭
}
  • 버튼을 눌렀을때 실행되어야 하는 코드는 위와 같이 짜보았다.
  • 3블럭이라고 주석친 부분들만 main.sync로 실행해보았다.

image

  • 2블럭까지는 잘 실행 되다 그 이후에 오류뜬 모습 !!

왜 오류가 뜨는 것이야 ❗️

image

image

  • 미래의 나에게 설명을 돕기위해 그림으로 그려보았다.
  • DispatchQueue.main.sync의 뜻을 생각해보자
  • main : main 스레드에서 실행하도록 하는 직렬큐 (순차적 실행큐) 로 배정
  • sync : 큐로 보낸 작업이 끝날때까지 다른애들은 동시에 일을 할 수 없음

image

  • 그렇기 때문에 3번째 블럭은 mainQueue에 맨 뒷부분으로 자리를 잡게되고 (큐의 특징 맨뒤에서 배정임)
  • sync 때문에 현재 작업을 큐로 보낸 스레드( 여기서는 메인스레드) 는 3번째가 처리 될 때까지 일을 하지 않는다.
  • 따라서 4번째 블럭은 절대로 실행되지 않는 DeadLock 발생
    • 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태
  • 4번째 블럭 때문이라 쉽게 이해하기 위해 말했지만 꼭 4번째 블럭이 실행안돼서 3번째 블럭이 실행 안되는건 아님
  • main 스레드는 모든 UI 처리를 담당하는 스레드 인건 상식으로 알고 있쥬?
  • 그렇기 때문에 꼭 4번째 블럭말고도 처리해야 하는 UI가 있을수 있으므로,
  • 애플은 Main Thread를 Thread-unsafe(교착상태가 발생할 수 있는 스레드)로 명시한다.

결론

  • Main 스레드는 unsafe스레드 이기 때문에
  • 메인스레드에서 main.sync하는 것은 데드락을 발생시키므로 에러가 뜸.
  • 끊임없이 앱의 이벤트 처리를 하고 있던 main thread가 sync호출에 의해 멈추게 되고
  • deadlock 발생
    • 상태란 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태
  • 즉 메인스레드에서 main.sync호출하면 deadlock ❗️


DispatchQueue.main.sync 가 반드시 안되는 것은 아님 ⭐️

  • 백그라운드 쓰레드에서 이루어지는 작업들 사이에 순서에 맞게
  • main 쓰레드에서 작업이 이루어져야 할 때 사용
  • Background thread 내에서 사용하는 것이 아니면 DispatchQueue.main.sync 사용 노노


그리고 팁: 코드로 Main Thread에서 global.sync는 사용을 거의 안한다. ( customQueue도 동시라면 마찬가지)

  • GCD에 대해 명확히 이해했다면 너무 당연한 이야기 아님..?
  • 메인스래드가 동기로 일을 다른 쓰레드에게 처리하라고 주면,
  • 어차피 메인 스레드는 그 일이 동시큐에서 처리 되어야 실행가능
  • 즉, 메인스레드에서 global.sync로 작업을 주면
  • 그냥 메인스레드에서 실행하는 거랑 ❗️똑같은❗️ 실행순서와 결과가 나옴


왜 병렬이라고 안하고 동시라고 말을 할까? 둘의 차이는 무엇?

  • 동시성(concurrency) 은 동시에 실행되는 작업을 말하며,
  • 병렬(parallel) 은 하나의 작업을 작은 단위로 나눠서 동시에 실행함을 의미한다.
  • 즉 동시가 더 큰 개념이라고 한다!


참고 자료