GCD에 대해 (종류, 특성)

JinSeok Hong·2021년 9월 27일
0

앞서 공부했던 비동기(Async), 동기(Sync), 직렬(Serial), 병행(Concurrency)를 iOS 개발에서는 어떻게 사용할까?
바로 GCD를 통해 큐에 스레드 분배하며 이러한 개념을 적용한다. 보통 메소드 위주의 작업에 이용되고 큐를 이용하여 스레드를 관리하기 때문에 FIFO의 형태를 띄고있다.


GCD의 종류


1. DispatchQueue.main

  • 유일한 메인 큐, 스레드를 의미한다.
  • 일반적으로 .async 비동기로서 이용한다.
  • 직렬(Serial)의 특성을 가지고 있다.

2. DispatchQueue.global

  • 병행(concurrency)으로 설정되어있다.
  • 6가지의 서비스 품질(QOS) 종류를 중요도에 따라 설정하여 큐를 만든다. (중요도 순)
    1. DispatchQueue.global(qos: .userInteractive)
      • UI, 애니메이션 등 유저와 직접적으로 상호작용하는 작업을 한다.
    2. DispatchQueue.global(qos: .userInitiated)
      • pdf 파일 여는 등 유저가 즉시 필요하긴 하지만 비동기적으로 처리하는 작업을 한다.
    3. DispatchQueue.global()
      • 일반적인 작업
    4. DispatchQueue.gloabl(qos: .utility)
      • 계산, IO, 네트워크, 지속적 데이터 feeds 등 어느 정도 로딩이 필요한 작업을 한다.
    5. DispatchQueue.gloabl(qos: .background)
      • 데이터 미리 가져오기, DB 유지 등 유저가 인지하지 않으면서도 속도보다는 효율성이 중요한 작업을 한다.
    6. DispatchQueue.gloabl(qos: .unspecified)

3. DistpatchQueue(label: "")

  • 커스텀으로 만듦.
  • 기본 설정 직렬(Serial)이지만 병행(Concurrent) 설정도 가능하다.
    • DispatchQueue(label: "", attributes: .concurrent)
  • QoS 설정도 가능

GCD 특성


UI 관련 일들은 Main Queue에서만 처리한다.

URLSession.shared.dataTask(with: url) { (data, response, error) in //URL Session은 내부적으로 비동기 처리되어있다.

	if let error = error {
		print("Failed to load image with error", error.localizedDescription)
	}
            
    	if self.lastImgUrlUsedToLoadImage != url.absoluteString {
       		return
   	}
            
    	guard let imageData = data else { return }
            
        let photoImage = UIImage(data: imageData)
            
        imageCache[url.absoluteString] = photoImage
            
        // UI 관련은 메인 스레드에서
        DispatchQueue.main.async {
   	     self.image = photoImage
       	}    
}.resume()

왜 UI 관련 작업은 메인 스레드에서만 처리해야할까?

  1. 간섭
    수많은 스레드에서 동시다발적으로 UI 업데이트 과정을 거친다면 각 스레드의 작업 처리에 서로가 간섭을 하게된다. 따라서 원하는대로 UI 처리가 되지 않을 수 있다.

  2. Response chain
    앱을 실행하면 Cocoa Touch에서 UIApplcation 인스턴스가 메인 스레드 내에서 설정된다. 따라서 responder chain을 따라 UIApplcation으로 main event loop를 통해 UI 이벤트들이 전달되는 과정이 메인 스레드에서 이루어진다.

  3. 성능
    iOS가 그림의 그리는 과정이 Rendering Process(CoreAnimation -> Render Server -> GPU -> Present)를 통해 이루어진다고 한다. 여기서 멀티스레드 환경으로 UI를 작업하면 수많은 렌더링 작업들이 보내지게 되고 이걸 GPU가 처리하기 리소스가 많이 들어 성능 저하가 생긴다고 한다.

클로저 사용


클로저를 사용할 때, 비동기 프로그래밍에서 작업 처리가 끝난 시점을 처리하기 위해 Completion Handler와 메모리 관리를 위해 캡처리스트 개념을 쓰게 된다. 캡처리스트는 응용하기 위해서는 순환참조와 ARC에 대해 알아야한다.
ARC는 Automatic Reference Counting 으로 자동 참조 카운팅이라는 의미이다. Heap 메모리에 할당되는 메모리를 관리하기 위해 사용하고 클래스의 인스턴스를 처리할 때 weak, strong 키워드를 이용한다.

DispatchQueue.global().async { [weak self] in
    guard let self = self else { return }
    
    DispatchQueue.main.async {
        self.textLabel.text = "New Posts Updated!"
    }
}

이처럼 Reference Counting을 올리지 않으며 강력 순환 참조를 벗어나 내부 self를 사용하기 위해 [weak self] 와 같은 코드를 이용하는데 왜 이러한 캡처 리스트를 사용할까?

차이는 weak 키워드는 인스턴스가 dismiss 되면 크로 보낸 클로저(작업)도 중단되지만 strong 키워드로 된 self는 dismiss되더라도 큐로 보낸 클로저(작업)이 동작하여 오류를 불러올 수 있다.
이전에 프로젝트를 하면서 아래와 같은 동작을 반복하면 앱이 죽어버리는 경우가 생겼다.

    lazy var backButton = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
        self?.navigationController?.popViewController(animated: true)
    })).then {
        $0.setImage(UIImage.btnBack.withRenderingMode(.alwaysTemplate), for: .normal)
        $0.tintColor = .white }

ViewController를 Push, pop하면서 버튼 초기화의 클로저 내부의 self 뷰컨에 메모리가 계속 쌓여 문제가 발생한 것이다.
당시 [weak self] 를 추가하여 뷰컨의 작업을 중단시키면서 메모리릭을 해결할 수 있었다.
GCD를 이용한 비동기 처리시, 순환참조를 잘 처리하여 메모리를 효율적으로 사용하는 것이 중요하다. 또한 completion handler를 통해 작업의 끝난 시점을 잘 파악하여 다음 작업을 처리할 수 있도록 해야한다.

    

0개의 댓글