[2019.03.12]

원문 : https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2

Grand Centrial Dispatch Tutorial for Swift 4: Part ½

다운로드

Grand Center Dispatch(GCD)는 동시에 발생하는(concurrent) 작업을 관리하는 저수준(low-level) API 입니다. 이는 연산이 많이 필요한 작업(expensive tasks)을 백그라운드에서 처리하도록 미뤄서(deffering) 앱의 응답속도를 향상시키는데 도움을 줄 수 있습니다. 잠금(locks)나 스레드(threads)보다 작업하기 더 쉬운 동시성 모델(concurrency model) 입니다.

2 부분으로 된 Grand Central Dispatch(GCD) 튜토리얼에서, GCD와 Swifty API의 입력과 출력을 배우게 될 것입니다. 첫번째 부분에서는 GCD가 하는 일이 무엇인지를 설명하고 몇가지 기본 GCD 함수들을 볼것입니다. 두번째 부분에서는 GCD에서 제공하는 몇가지 고급 함수들을 배우게 될 것입니다.

기존 앱 GooglyPuff을 기반으로 할 것입니다. GoolyPuff는 최적화되어 있지 않으며, 코어 이미지(Core Image)의 얼굴 인식 API를 사용해서 감지된 얼굴에 왕방울 눈(googly eyes)을 겹쳐서 표시하는 스레드에 안전하지 않은(thread-unsafe) 앱입니다. 이 효과를 적용하기 위해서 사진 라이브러리에서 이미지를 선택하거나 인터넷에서 다운로드 받은 이미지를 선택할 수 있습니다.

이 튜토리얼에서 여러분의 임무는, GCD를 사용해서 앱을 최적화하고 다른 스레드에서 코드를 안전하게 호출할 수 있도록 적용하는 것입니다.

시작하기(Getting Started)

튜토리얼의 상단이나 하단에 있는 다운로드 버튼을 사용해서 시작 프로젝트를 다운로드 합니다. Xcode에서 열고 실행해서 무엇을해야 하는지 보세요.

홈 화면이 처음에는 비어있습니다. +를 탭하고 인터넷에서 미리 정의된 이미지를 다운로드하기 위해 Le Internet을 선택합니다. 첫번째 이미지를 누르고 얼굴에 왕방울 눈(googly eyes)이 추가되어 있는것을 보게 될 것입니다.

여러분은 이 튜토리얼에서 주로 4개의 클래스로 작업하게 될것입니다.

  • PhotoCollectionViewController : 초기 뷰 컨트롤러입니다. 썸네일으로 선택된 이미지를 보여줍니다.
  • PhotoDetailViewController : PhotoCollectionViewController로 부터 선택된 사진을 보여주고 이미지에 왕방울 눈(googly eyes)을 추가합니다.
  • Photo : 사진의 프로퍼티들을 설명하는 프로토콜입니다. 이미지, 썸네일, 해당 상태를 제공합니다. 이 프로젝트는 프로토콜을 구현하는 2개의 클래스를 포함합니다: URL의 인스턴스로부터 사진을 인스턴스화 하는 DownloadPhotoPHAsset의 인스턴스로부터 사진을 인스턴스화 하는 AssetPhoto.
  • PhotoManager : 모든 Photo 객체들을 관리합니다.

앱은 몇가지 문제가 있습니다. 앱을 실행할때 여러분이 눈치챘을 수도 있는 하나는 다운로드 완료 알림이 너무빠르다(premature)는 것입니다. 이 시리즈의 두번째 부분에서 이를 고칠 것입니다.

첫뻔째 부분에서, gooly-fying 프로세스를 최적화하고 PhotoManager를 스레드에 안전하게 하는 몇가지 개선 작업을 할 것입니다.

GCD 개념(GCD Concepts)

GCD를 이해하기 위해서, 동시성(concurrency)과 스레딩(threading)과 관련된 몇가지 개념에 대해서 익숙해져야 합니다.

동시성(Concurrency)

iOS에서, 프로세스나 앱은 하나 이상의 스레드로 구성됩니다. 운영체제 스케쥴러는 스레드를 서로 독립적으로 관리합니다. 각 스레드는 동시에 실행할수 있지만, 이런 일이 생기게 되는 경우, 일이 발생할때, 그리고 어떻게 되는지는 시스템이 결정합니다.

단일 코어 기기(single-core devices)는 시간 쪼개기(time-slicing)이라는 메소드를 통해서 동시성을 처리합니다. 하나의 스레드를 실행하고, 문맥 전환(context switch)을 수행해서, 다른 스레드를 실행합니다.

반면에, 멀티 코어 기기(multi-core devices는, 병렬처리(parallelism)를 통해서 동시에 여러개의 스레드를 실행합니다.

GCD는 스레드를 기반으로 만듭니다. 내부적으로(hood) 공유 스레드 풀을 관리합니다. GCD로 dispatch queues에 코드 블럭이나 작업 항목을 추가하고 GCD는 어던 스레드가 실행할지를 결정합니다.

코드를 구조화하는 것 처럼, 동시에 실행할수 있는 코드 블럭과 그렇지 않은 코드 블럭을 찾게 될것입니다. 그리고나서 GCD를 사용해서 동시에 실행하는 이점을 얻을수 있습니다.

GCD는 시스템과 시스템이 사용가능한 리소스를 기반으로 얼마만큼 병렬 처리를 할 것인지 결정하는 것을 기억합니다. 병렬 처리에는 동시성이 필요(requires) 하지만, 동시성이 병렬처리하는 것을 보장(guarantee)하지는 않는다는 것을 기억하는 것이 중요합니다.

기본적으로, 동시성(concurrency)은 구조(structure)에 대한 것이며, 병렬처리는 실행(execution)에 대한 것입니다.

Queues

이전에 언급했듯이, GCD는 DispatchQueue라는 이름의 클래스를 통해서 dispatch queues에서 동작합니다. 작업 단위를 큐(queue)에 넣고 GCD는 FIFO(First In, First Out) 순서로 실행하며, 제출된(submitted) 첫번쩨 작업이 첫번째로 시작되는 것을 보장합니다.

Dispatch Queue는 스레드에 안전(thread-safe)하며, 여러 스레드에서 동시에 사용할 수 있는 것을 의미합니다. GCD의 장점은 dispatch queue가 자신의 코드 부분에서 스레스 안전성을 제공하는 방법을 이해할때 명백해집니다. 핵심은 큐(queue)에 작업을 넣기(submit) 위해서, 올바른 dispatch queue의 종류와 올바른 dispatching function를 선택하는 것입니다.

큐(Queues)는 직렬(serial) 또는 동시성(concurrent) 중 하나가 될 수 있습니다. 직렬 큐(Serial queues)는 주어진 시간에 하나의 작업만 실행하는 것을 보장합니다. GCD는 실행 타이밍을 제어합니다. 하나의 작업이 끝나고 다음 작업이 시작하는 사이의 시간을 알 수 없을 것입니다.

동시성 큐(Concurrent queue)는 동일한 시간에 여러개의 작업을 실행하는 것을 허용합니다. 큐는 추가한 순서대로 작업을 시작하는 것을 보장합니다. 작업들은 순서와 상관없이 완료될 수 있고 다음 작업을 시작하는데 걸릴 시간에 대한 지식이 없으며, 주어진 시간에 실행되는 작업의 갯수도 모릅니다.

이는 의도적으로 설계되어있습니다: 코드가 이러한 구현의 세부사항에 의존해서는 안됩니다.

아래 샘플 작업 실행을 보세요.

작업(Task) 1, 작업(Task) 2, 작업(Task) 3이 차례대로 빠르게 시작하는 방법을 주의합니다. 반면에, 작업 1은 작업 0 이후에 시작하는데 시간이 걸립니다. 또한 작업 3은 작업 2 이후에 시작했지만, 먼저 완료됩니다.

작업을 시작하는 시기는 전적으로 GCD에 의해 결정됩니다. 하나의 작업이 다른 작업의 실행 시간과 겹치게 되는 경우, 다른 코어에서 실행해야 하는 지, 사용할 수 있는지, 또는 다른 작업을 실행하기 위해 문맥 교환(context switch)를 수행하는지 결정하는 것은 GCD에 달려있습니다.

GCD는 3가지 주요 타입의 큐(queues)를 제공합니다.

  1. Main queue : 메인(main) 스레드에서 실행하고 직렬(serail) 큐입니다.
  2. Global queues : 전체 시스템에 의해 공유되는 동시 큐(concurrent queues). 우선순위가 다른 4개의 큐(queues)가 있습니다 : high, default, low, background. 백그라운드(background) 우선순위 큐는 가장 낮은 우선순위이고 시스템에 나쁜 영향을 최소화하기 위해서 모든 입출력(I/O) 동작을 제한(throttled)합니다.
  3. Custom queues : 여러분이 생성하는 큐이며, 직렬 또는 동시성 큐가 될 수 있습니다. 이러한 큐의 요청은 실제로 전역 큐(global queues) 중에 하나로 끝이 납니다.

전역 동시 큐(global concurrent queues)에 작업을 보낼때, 직접 우선순위를 지정하지 않습니다. 대신에, Quality of Service(QoS) 클래스 프로퍼티를 지정합니다: 이는 작업의 중요성을 나타내고 GCD가 작업에 부여할 우선순위를 결정하는 것을 도와줍니다.

Qos 클래스는 다음과 같습니다:

  • User-interactive : 멋진 사용자 경험을 제공하기 위해서 즉시 완료해야만 하는 작업을 나타냅니다. UI 업데이트, 이벤트 처리와 낮은 대기시간을 필요로하는 작업량에서 사용합니다. 앱을 실행중에 이 클래스로 실행하는 총 작업량은 작아야 합니다. 메인 스레드에서 실행되야 합니다.
  • User-initiated : UI에서 사용자가 비동기 작업을 시작합니다. 사용자가 즉각적인 결과와 사용자 상호작용을 계속하기 위해서 필요한 작업을 기다릴때 사용합니다. 우선 순위가 높은 전역 큐(global queue)에서 실행합니다.
  • Utility : 장시간 실행하는 작업을 표현하며, 일반적으로 사용가 볼수 있는 진행 표시기(progress indicator)가 있습니다. 계산, I/O, 네트워킹, 연속되는 데이터 가져오기와 비슷한 작업에서 사용합니다. 이 클래스는 에너지에 효율적이도록 설계되었습니다. 이는 우선순위가 낮은 전역 큐(global queue)와 매핑됩니다.
  • Background : 사용자가 직접 알지 못하는 작업을 표현합니다. 미리가져오기(prefetching), 유지보수(maintenance), 사용자 상호작용이 필요하지 않고 시간에 민감하지 않은 다른 작업에 사용합니다. 이는 우선순위가 백그라운드 전역 큐(global queue)와 매핑됩니다.

동기 vs 비동기(Synchronous vs. Asynchronous)

GCD에서, 동기 또는 비동기 중 하나로 작업을 처리할 수 있습니다.

동기(synchronous) 함수는 작업을 완료한 후에 호출자에게 제어권을 반환합니다. DispatchQueue.sync(execute:)를 호출해서 동기(synchronously) 작업 단위로 스케쥴 할 수 있습니다.

비동기(asynchronous) 함수는 작업을 시작하자마자 즉시 반환하지만, 완료되는 것을 기다리지 않습니다. 그러므로(Thus), 비동기 함수는 현재 실행하는 스레드가 다음 함수를 진행하는 것을 차단하지 않습니다. DispatchQueue.async(execute:)를 호출해서 비동기 작업 단위로 스케쥴 할 수 있습니다.

작업 관리하기(Managing Tasks)

지금가지 작업데 대해서 많이 들어봤습니다. 이 튜토리얼의 목적상, 작업(task)을 클로져(closure)로 간주 할 수 있습니다. 클로져는 스스로 포함되며, 저장하고 전달할 수 있는 호출 가능한 코드 블럭입니다.

DispatchQueue에 제출한 각 작업은 DispatchWorkItem입니다. Qos 클래스로 DispatchWorkItem의 동작 또는 새로 분리된 스레드를 생성할 지를 구성할 수 있습니다.

백그라운드 작업 처리하기(Handling Background Tasks)

축적된 GCD 지식으로, 첫번째 앱 개선을 할 시간입니다.

앱으로 돌아가고 사진 라이브러리 또는 Le Internet 옵션을 사용해서 몇가지를 다운로드해서 약간의 사진을 추가합니다. 사진(photo)을 탭합니다. 사진 상세 뷰가 보여지는데 얼마나 걸리는지 확인하세요. 지연은 느린 장치에서 커다란 이미지를 보여줄때 더 두드러집니다.

뷰 컨트롤러의 viewDidLoad() 과부화(overloading)하기 쉬우며, 뷰가 나타나기 전에 오랫동안 기다려야 합니다. 로딩 시점에 필요하지 않는 것들을 백그라운드로 작업량을 줄이는것이 최선입니다.

이는 DispatchQueue의 async 작업과 같습니다.

PhotoDetailViewController.swift를 엽니다. viewDidLoad()를 수정하고 다음 두줄을 교체합니다.

let overlayImage = faceOverlayImageFrom(image)
fadeInNewImage(overlayImage)

다음 코드로 교체합니다.

// 1
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  guard let self = self else {
    return
  }
  let overlayImage = self.faceOverlayImageFrom(self.image)

  // 2
  DispatchQueue.main.async { [weak self] in
    // 3
    self?.fadeInNewImage(overlayImage)
  }
}

다음은 코드가 단계별로 무엇을 하는지 입니다.

  1. 백그라운드 전역 큐로 작업을 옮기고 비동기적인 클로져에서 작업을 실행합니다. 이렇게하면 viewDidLoad() 가 메인 스레드에서 일찍 완료되고 로딩하는 것이 더 짧아지게 만듭니다. 그 동안에, 얼굴 감지 프로세스는 시작되고 어느정도 시간이 지난후에 종료할 것입니다.
  2. 이 시점에, 얼굴 감지 프로세스는 완료되고 새로운 이미지가 생성됩니다. UIImageView에 새로운 이미지를 사용해서 업데이트하기 위해, 메인 큐에 새로운 클로져를 추가합니다. UI를 수정하는 모든 것은 메인 스레드에서 실행되야 하는 것을 기억해야 합니다!
  3. 마지막으로, 얼굴에 새로운 왕방울 눈(googly)으로 전환하는 것을 수행하는 fadeInNewImage(_:)로 UI를 업데이트 합니다.

두 곳에서, 각 클로져에서 self의 약한 참조를 캡쳐하기 위해서 [weak self]를 추가했습니다. 캡쳐 목록이 익숙하지 않는 경우에는 메모리 관리를 확인하세요.

빌드하고 앱을 실행합니다. Le Internet 옵션으로 사진을 다운로드 합니다. 사진을 선택하고 뷰 컨트롤러가 눈에 띄게 빨라지고 잠시 뒤에 왕방울 눈(googly eyes)이 추가되는 것을 알 수 있습니다.

왕방울 눈이 나타나는 것처럼 앱에 이전과 이후에 효과를 줍니다. 심지어는 미친듯이 거대한 이미지를 로딩하려는 경우에도, 앱은 뷰컨트롤러가 로딩될때 중단되지 않습니다.

일반적으로, 백그라운드에서 네트워크 기반 또는 CPU 집중적인 작업을 수행하고 현재 스레드를 막지 않아야 할때 async를 사용합니다.

다음은 asyn로 다양한 뷰를 사용하기 위해 언제 어떻게 해야하는지에 대한 빠른 가이드입니다.

  • Main Queue : 일반적으로 동시성 큐(concurrent queue)에서 작업을 완료한 후에 UI를 업데이트할때 선택합니다. 이를 위해서, 하나의 클로져를 다른 클로져 안에 코딩합니다. 메인 큐를 대상으로 하고 async를 호출하면 현재 메소드가 완료된 후에 언젠가는 새로운 작업이 실행될 것입니다.
  • Global Queue : 일반적으로 백그라운드에서 UI가 아닌 작업을 수행할때 선택합니다.
  • Custom Serial Queue : 백그라운드 작업을 순차적으로 수행하고 추적할때 좋은 선택입니다. 이렇게 하면 한번에 하나의 작업만 실행하므로 리소스 경합(contention)이나 경쟁 조건이 제거 됩니다. 메소드의 데이터가 필요한 경우에, 그것을 구하기 위해 반드시 다른 클로져를 선언해야 하거나 sync를 사용하는 것을 고려해야 합니다.

작업 실행 지연하기(Delaying Task Execution)

DispatchQueue는 작업 실행을 지연하는 것을 허용합니다. 경쟁 조건이나 다른 타이밍적인 버그를 해결하기 위해 지연을 도입한 것 같이 엉망으로(hack) 만들지 마세요. 대신에, 특정 시간에 작업을 실행하려고 할때 사용하세요.

잠깐동안 앱의 사용자 경험을 고려해보세요. 사용자가 앱을 처음 열었을때 해야할 일이 무엇인지 혼란스러울 수도 있습니다. - 그랬나요? :]

아무런 사진이 없는 경우에 사용자에게 프롬프트를 표시하는 것이 좋을 것입니다. 또한 사용자의 눈이 홈 화면을 어떻게 탐색할것인지를 고려해야 합니다. 프롬프트(prompt)를 너무 빨리 표시하는 경우, 뷰의 다른 부분에서 눈이 떠올라서 놓칠수도 있습니다. 2초 지연이 사용자의 주의를 끌고 안내하기에 충분해야 합니다.

PhotoCollectionViewController.swift를 열고 showOrHideNavPrompt()에 대한 구현을 채워넣습니다.

// 1
let delayInSeconds = 2.0

// 2
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [weak self] in
  guard let self = self else {
    return
  }

  if PhotoManager.shared.photos.count > 0 {
    self.navigationItem.prompt = nil
  } else {
    self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
  }

  // 3
  self.navigationController?.viewIfLoaded?.setNeedsLayout()
}

위에서 무엇을 하는지는 다음과 같습니다.

  1. 지연하기 위한 시간을 지정합니다.
  2. 지정된 시간만큼 기다리고나서 사진 갯수를 업데이트하고 프롬프트를 업데이트하는 비동기적으로 블록문을 실행합니다.
  3. 프롬프트를 설정한 후에 정교하게 보이도록하기 위해 네비게이션 바를 강제로 배치합니다.

viewDidLoad()와 UICollectionView가 다시 로딩될때 showOrHideNavPrompt()를 실행합니다.

빌드하고 앱을 실행합니다. 프롬프트가 표시되기 전까지 약간 지연이 있어야 합니다.

주의: Xcode 콘솔에서의 오토레이아웃 메시지는 무시할수 있습니다. 그것들은 iOS에서 모두 나온것이고 여러분이 실수한 것이 아닙니다.

왜 Timer를 사용하지 않나요? 반복되는 작업이 있는 경우에는 사용하는것을 고려할 수 있으며, Timer로 스케쥴하기 쉽습니다. 다음은 dispatch queue의 asyncAfter()를 사용하는 2가지 이유가 있습니다.

하나는 가독성(readability)입니다. Timer를 사용하기 위해서 메소드를 정의해야 하며, 정의된 메소드를 셀렉터(selector)하거나 호출하는 타이머를 만듭니다.

Timer는 반복문을 실행해서 스케쥴 되므로, 올바른 반복문 실행(그리고, 경우에 따라 올바른 반복문 모드)에서 스케쥴 되어있는지 확인해야 합니다. 이와 관련해서, dispatch queue를 사용해서 작업하는 것이 더 쉽습니다.

싱글톤 관리하기(Managing Singletons)

싱글톤(Singletons). 사랑하거나 미워하며, iOS에서 웹상의 고양이 사진처럼, 인기가 있습니다. :]

싱글통에 대한 한가지 우려(concern)되는 사항은 종종 스레드에 안전하지 않는 것입니다. 이 우려(concern)는 그것들을 사용할때 당연합니다 : 싱글톤은 종종 싱글톤 인스턴스를 동시에 여러개의 컨트롤러에서 사용됩니다. PhotoManager 클래스는 싱글톤이므로, 이 이슈에 대해서 고려해야 할 것입니다.

스레드에 안전한 코드는 데이터 손상이나 앱 충돌과 같은 문제를 일이키지 않고, 여러개의 스레드 또는 동시성인 작업에서 안전하게 호출될수 있습니다. 스레드에 안전하지 않는 스레드는 한번에 하나의 컨텍스트에서만 실행할 수 있습니다.

고려해야 할 스레드에 안전한 2가지의 경우가 있습니다: 싱글톤 인스턴스가 초기화되는 동안과 인스턴스를 읽고 쓰는 동안

Swift가 정적 변수를 초기화하는 방법 때문에 초기화가 쉬운 경우로 판명됩니다. 최초 사용할때 정적 변수를 초기화 하고, 초기화가 원자(atomic)임을 보증합니다. 즉, Swift가 임계 구역(critical section)으로 초기화를 수행하고 다른 스레드가 정적 변수에 접근하기 전에 코드를 완료하는 것을 보장합니다.

임계 구역(critical section)은 한 번에 2개의 스레드에서, 동시에 실행해서는 안되는 코드 조각입니다. 이는 일반적으로 코드가 동시성 프로세스에 의해 사용될때 변경되는 변수처럼, 공유 자원을 조작하기 때문입니다.

싱글톤을 초기화하는 방법을 보기 위해 PhotoManager.swift를 엽니다.

class PhotoManager {
  private init() {}
  static let shared = PhotoManager()
}

전용 초기화는 유일한 PhotoManager를 만들어 shared에 한번 할당됩니다. 이 방법은, 서로 다른 관리자간에 사진 저장소에 변경사항을 동기화 하는 것에 대해 걱정할 필요가 없습니다.

공유된 내부 데이터를 조작하는 싱글톤에서 코드를 사용할때 여전히 스레드에 안전하도록 처리해야 합니다. 동기 데이터 사용하기 처럼, 메소드를 통해서 이를 처리 할 수 있습니다. 다음 섹션에서 한가지 방법을 보게 될 것입니다.

읽고 쓰는 문제 처리하기(Handling the Readers-Writers Problem)

Swift에서, 모든 변수는 let 키워드로 상수로 선언되고, 따라서 읽기 전용이고 스레드에 안전합니다. var 키워드로 변수를 선언하고, 데이터 타입이 그렇게 설계되지 않는 한, 변경가능하고 스레드에 안전하지 않습니다. Array와 Dictionary와 같은 Swift 컬렉션 타입은 변경가능함(mutable)으로 선언될때 스레드에서 안전하지 않습니다.

많은 스레드가 문제 없이 Array의 변경가능한 인스턴스를 동시에 읽을 수 있지만, 하나의 스레드가 다른 스레드를 읽는 동안에 배열을 수정하는 것은 안전하지 않습니다. 싱글톤은 현재 상태에서 이런 일이 발생하는 것을 막지 않습니다.

그 문제를 보기 위해, PhotoManager.swift에 있는 addPhoto(_:)를 보며, 아래는 복사된 것입니다.

func addPhoto(_ photo: Photo) {
  unsafePhotos.append(photo)
  DispatchQueue.main.async { [weak self] in
    self?.postContentAddedNotification()
  }
}

이것은 변경가능한 배열 객체를 수정하기 위해 write 메소드입니다.

이제 photos 프로퍼티를 살펴보며, 아래는 복사된 것입니다.

private var unsafePhotos: [Photo] = []
  
var photos: [Photo] {
  return unsafePhotos
}

이 프로퍼티에 대한 getter는 가변 배열을 읽는 것처럼 read 메소드라고 합니다. 호출자는 배열의 복사본을 받고 원래 배열을 부적절하게 변경하는 것으로 부터 보호됩니다. 하지만, 이것은 photos프로퍼티에 대해 다른 스레드가 동시에 getter을 호출하는 동안에, 쓰기 메소드라 불리는 addPhoto(_:)를 호출하는 하나의 스레드에 대해서 어떠한 보호도 제공하지 않습니다.

그것이 변수가 unsafePhotos 이름인 이유입니다 - 잘못된 스레드에서 사용하는 경우, 이상한 동작이 발생할 수 있습니다!

주의
위 코드에서, 호출자가 왜 photos 배열의 복사본을 얻을까요? Swift에서, 함수의 매개변수와 반환 타입은 참조 또는 값으로 전달 됩니다.

값을 전달하면 객체의 복사본이 되고, 그 복사본에 대한 변경사항은 원본에 영향을 주지 않습니다. Swift는 기본적으로 class 인스턴스는 참조로 전달되고 struct는 값으로 전달됩니다. ArrayDictionary과 같은 Swift의 내장된 데이터 타입은 struct로 구현되어 있습니다.

컬렉션을 앞 뒤로 전달할때, 코드에서 복사하는 것이 많은 것으로 보일 수 있습니다. 이러한 메모리 사용에 대해서 걱정하시 마세요. Swift 컬렉션 타입은 필요한 경우에만 복사본을 만들도록 최적화 되어 있으며, 예를 들어, 앱이 처음으로 값으로 전달된 배열을 수정할때 입니다.

이는 고전적인 소프프퉤어 개발의 Readers-Writers Problem입니다. GCD는 dispatch barriers를 사용해서 read/write lock을 생성하는 우아한 방법을 제공합니다. dispatch barriers는 동시성 큐(concurrent queues)로 작업할때 직렬 방식의 병목현상(bottleneck)으로 동작하는 함수의 그룹입니다.

DispatchWorkItem을 dispatch queue에 넣을때 특정 시간동안 지정된 큐에서 실행되는 유일한 항목임을 나타내는 플래그(flags)를 설정할 수 있습니다. 이는 dispatch barrier 전에 큐에 있는 모든 항목은 DispatchWorkItem이 실행되기 전에 완료되야 합니다.

DispatchWorkItem 차례가 될때, barrier는 그것을 실행하고 그 시간동안 큐(queue)가 다른 작업을 실행하지 않도록 보장합니다. 일단 끝나면, 그 큐(queue)는 기본 구현으로 돌아갑니다.

아래 그림은 다양한 비동기 작업에 대한 barrier의 효과를 보여줍니다.

일반적으로 동작하는큐(queue)가 어떻게 일반적인 동시성 큐(concurrent queue)처럼 동작하는지를 주의합니다. barrier가 실행중일때, 본질적으로 직렬 큐처럼 동작합니다. 즉, barrier는 하나만 실행합니다. barrier이 끝난뒤에, 큐(queue)는 정상적인 동시 큐(concurrent queue)로 돌아갑니다.

이러한 큐(queue)를 공유 리소스로 전역 백그라운드 동시성 큐(concurrent queue)에서 barriers를 사용할때에는 주의해야 합니다. 사용자정의 직렬 큐(serial queue)에서 barriers를 사용하는 것은 이미 직렬로 실행되므로 중복됩니다. 사용자정의 동시성 큐(concurrent queue)에서. barriers를 사용하는 것은 원자(atomic) 또는 임계 영역(critical area)에서 스레드 안전성을 처리하는데 훌륭한 선택입니다.

barrier 함수를 처리하기 위해 사용자정의한 동시성 큐(concurrent queue)를 사용할 것이고 읽고 쓰는 함수를 분리합니다. 동시성 큐(concurrent queue)는 동시에 여러개의 읽기 작업을 허용합니다.

PhotoManager.swift를 열고 unsafePhotos 선언 바로 위에 private 프로퍼티를 추가합니다.

private let concurrentPhotoQueue =
  DispatchQueue(
    label: "com.raywenderlich.GooglyPuff.photoQueue",
    attributes: .concurrent)

동시성 큐(concurrent queue)로 concurrentPhotoQueue를 초기화 합니다. 디버깅중에 도움이되는 설명하는 이름을 label로 설정합니다. 일반적으로 역 DNS 스타일 이름 규칙을 사용합니다.

다음으로, 다음에 오는 코드로 addPhoto(_:)를 교체합니다.

func addPhoto(_ photo: Photo) {
  concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
    // 1
    guard let self = self else {
      return
    }

    // 2
    self.unsafePhotos.append(photo)

    // 3
    DispatchQueue.main.async { [weak self] in
      self?.postContentAddedNotification()
    }
  }
}

다음은 새로운 쓰기 메소드 작업하는 방법입니다.

  1. barrier로 비동기 쓰기 연산을 전달(dispatch)합니다. 그것이 실행될때, 여러분의 큐(queue)에 있는 유일한 항목이 될것입니다.
  2. 배열에 객체를 추가합니다.
  3. 마지막으로 새로운 사진이 추가되었다는 알림을 보냅니다. UI 작업을 할것이기 때문에, 메인스레드에서 알림을 보내야 합니다. 알림을 발생기 위해 메인 큐에서 다른 비동기 작업을 전달합니다.

이는 쓰기를 보호하지만, photos 읽기 메소드에 구현이 필요합니다.

쓰기가 스레드에서 안전함을 보장하기 위해, concurrentPhotoQueue 큐에서 읽기 작업을 해야만 합니다. 함수 호출에서 반환 데이터가 필요하므로 비동기적인 전달을 그만두지는 않을 것입니다. 이 경우에, sync는 훌륭한 후보자(candidate)가 될 것입니다.

dispatch barriers로 작업을 추적하기 위해 sync를 사용하거나 클로져에 의해 처리한 데이터를 사용하려면 작업이 완료될때까지 기다려야 합니다.

조심해야할 필요가 있습니다. sync를 호출하고 현재 큐를 대상으로 이미 실행중인 것으로 상상해 보세요. 이는 교착(deadlock) 상태가 발생할 것입니다.

2가지(또는 그 이상) 항목(대부분의 경우, 스레드)이 서로 완료되기를 기다리거나 다른 동작을 수행하길 기다리게 되면 교착(deadlock) 상태가 됩니다. 첫번째는 두번째가 완료되길 기다리기 때문에 완료할 수 없습니다. 하지만 두번째는 첫번째가 완료되길 기다리기 때문에 완료할 수 없습니다.

이 경우에, sync 호출은 클료져가 종료될때까지 기다릴 것이지만, 클로져는 현재 실행중인 클로져가 종료할때까지 종료(또는 시작)되지 않습니다. 이는 어떤 큐를 호출하고 있는지를 자각하기 위해 강제화 해야 합니다 - 어떤 큐를 전달할지 알려줄 것입니다.

다음을 sync를 사용하는 시점과 장소에 대한 간략한 개요입니다.

  • Main Queue : 위에서와 같은 이유를 매우(VERY) 조심해야 합니다; 이 상황은 교착(deadlock) 상태가 발생할 수도 있습니다. 전체 앱이 응답하지 않게 될것이기 때문에, 메인 큐에서 특히 나쁩니다.
  • Global Queue : dispatch barriers로 동기 작업을 하거나 작업이 완료되기를 기다릴때 추가 처리를 하기 위한 좋은 후보자입니다.
  • Custom Serial Queue : 이 상황을 매우(VERY) 조심해야 합니다; 큐에서 실행중이고 같은 큐를 대상으로 sync를 호출하는 경우, 새로운 교착(deadlock) 상태를 만들게 될 것입니다.

PhotoManager.swift 에서 photos 프로퍼티 getter를 수정합니다.

var photos: [Photo] {
  var photosCopy: [Photo]!

  // 1
  concurrentPhotoQueue.sync {

    // 2
    photosCopy = self.unsafePhotos
  }
  return photosCopy
}

다음은 단계별로 무엇을 하는지 입니다.

  1. concurrentPhotoQueue에서 읽기를 수행하기 위해 동기적(synchronously)으로 전달(dispatch)합니다.
  2. photosCopy에 있는 사진 배열의 복사본을 저장하고 그것을 반호나합니다.

빌드하고 앱을 실행합니다. Le Internet 옵션으로 사진을 다운로드합니다. 이전과 같이 동작해야하지만, 내부적으로 매우 좋은 스레드를 가지고 있습니다.

축하합니다 - PhotoManager 싱글톤은 이제 스레드에 안전합니다! 사진을 읽거나 쓰는 방법에 상관없이, 놀라는 일 없이 안전할 것이라고 확신 할 수 있습니다.

여기에서 어디로가야하나요?(Where to Go From Here?)

Grand Central Dispatch 튜토리얼에서, 스레드에 안전한 코드를 만드는 방법과 CPU 집약적인 작업을 하는 동안 메인 스레드의 응답을 유지하는 방법을 배웠습니다.

프로젝트의 완성된 버젼을 튜토리얼 위와 아래에 있는 Download버튼을 사용해서 다운로드 할수 있습니다. 튜토리얼에서 개선된 모든 것이 포함되어 있습니다. 이 튜토리얼의 두번재 부분에서 이 프로젝트를 계속 개선할 것입니다.

자신의 앱을 최적화하려는 경우에, Xcode에 내장된 Time Profile로 작업을 프로파일링(profiling)해야 합니다. Instrument를 사용하는 것은 이 튜토리얼의 범위를 벗어납니다.

동시성 대 병렬성(Concurrency vs Parallelism)에 대한 Rob Pike의 훌륭한 이야기를 확인해 보세요.

iOS Concurrency with GCD and Operations 비디오는 이 튜토리얼에서 다룬 동일한 주제가 많이 포함되어 있습니다.

이 튜토리얼의 다음 부분에서는 더 멋진 작업을 수행하기 위해 GCD API에 대해 자세히 살펴볼 것입니다.

Posted by 까칠코더