제목: All about Concurrency in Swift - Part 1: The Present

역자: 이 시리즈물의 2편이 나왔습니다! 조만간 번역할 예정이에요.

현재 배포된 스위프트 언어에서는 Go나 Rust가 한것 처럼 아직 네이티브 동시성 기능을 가지지 않는다.

작업들을 동시에 실행시키고 싶을때 경쟁상태의 결과를 다뤄야 한다면, 여러분이 할 수 있는 선택지는 몇개가 없다. libDispatch같은 외부 라이브러리를 사용하던지, 아니면 Foundation이나 OS가 제공하는 동기화 프리미티브(primitives)를 사용하는 것이다.

이 시리즈물의 첫번째 파트는, 스위프트3에서 우리가 처한 상황을 보고, Foundation의 락, 스레드, 타이머부터 언어의 게런티에대한 모든것과 최근에 만들어진 Grand Central Dispatch와 Operation Queues를 다룬다.

몇가지 기본 동시성 이론과 일반적인 동시성 패턴도 설명하게 될 것이다.

크리티컬 섹션과 동시 실행크리티컬 섹션과 동시 실행


스위프트가 돌아가는 모든 플랫폼에서 pthread를 사용할 수 있을지라도 이 라이브러이의 기능과 프리미티브는 이 글에서 설명하지 않을 것이며, 그것보다 더 높은 수준의 대안에대해 이야기할 것이다. NSTimer 클래스도 역서 이야기 하지 않으니 스위프트 3에서 어떻게 이것을 사용하는지 여기서 확인하자.

이미 여러번 발표했듯, 스위프트 4 이후의 주요 배포중 하나(꼭 스위프트 5는 아닐 것임)에서 더 나은 메모리 모델(memory model)을 정의하고, 새로운 네이티브 동시성 기능을 넣기위해 이 언어를 확장할 것이다. 새로운 동시성 기능은 외부 라이브러리없이 동시성 및 병렬처리를 다룰 수 있게 해주며, 동시성에대해 스위프트스러운 이상적인 방법을 정의할 것이다.

이것은 이 시리즈물의 다음 글의 주제가 될 것인데, 다른 언어에서 구현한 몇가지 대안의 방법과 패러다임을 토론하고, 이것들이 어떻게 스위프트로 구현될 수 있는지 이야기하게 된다. 그리고 오늘달에 이미 사용할 수 있는 몇가지 오픈소스의 구현을 분석하여 현재 배포된 스위프트로 Actors 패러다임, Go의 CSP 채널, Software Transactional Memory등을 이용할 수 있게 해줄 것이다.

이 두번째 글은 완전히 추측적인 것이다. 글의 주된 목표는, 이 주제에대해 소개해주어서 당신이 동시성을 어떻게 다룰지 정의하는 미래의 스위프트 배포에서 토론에 참여할 수 있게 해주는 것이다.

이글이나 나른 글의 Playground는 GitHub 나 Zipped에서 이용할 수 있다.

목차


멀티스레딩과 동시성 입문
오늘날 어떤 어플리케이션을 만들든 상관없이, 곧(혹은 훗날) 당신의 앱은 멀티스레드 실행의 환경에서 동작할 것이라는 사실을 고려해주어야한다.

하나 이상의 프로세서를 가진 컴퓨팅 플랫폼. 혹은 하나 이상의 하드웨어 실행 코어를 가진 프로세서는 10여년동안 우리 주변에 바짝 다가왔고 스레드프로세스 같은 개념은 나이를 먹어버렸다.

운영체제는 다양한 방법으로 사용자 프로그램에게 이 기능들을 제공해왔고, 모든 현대의 프레임워크나 앱은 유연성과 성능을 높히기위해 몇가지 잘 알려진 디자인 패턴들을 구현할 것이다. 그 중에는 다중 스레드도 포함되있다.

스위프트에서 어떻게 동시성을 다루는지 구체적으로 들어가보기전에, Dispatch QueuesOperation Queues를 사용할 때 필요한 기본 개념을 간단하게 설명하려 한다.

먼저 애플 플랫폼과 프레임워크가 스레드를 사용할지라도 왜 이것을 여러분의 어플리케이션에 넣으려하는지 먼저 질문해보아야한다.

일반적인 상황에서 다중스레드가 해결책이 될 수 있는 몇가지가 있다.
  • 작업 그룹 분리: 스레드는 실행 플로우의 관점에서 여러분의 어플리케이션을 모듈화하는데 사용할 수 있고, 각 스레드들은 예측할 수 있는 방법으로 같은 타입의 작업 그룹을 실행시키는데 사용할 수 있다. 여러분의 프로그램을 다른 실행 플로우로부터 고립시켜 앱의 현재 상태에대해 더 쉽게 만든다.
  • 데이터-독립의 컴포넌트들의 병렬화: 하드웨어 스레드를 지원받거나 아닌(다음에 보자) 다중 소프트웨어 스레드는 원래 입력 데이터 구조의 하위집합에서 작동하는 여러 동일한 작업 본사본들을 병렬화하는데 사용될 수 있다.
  • 조건이나 I/O를 기다리는데 깔끔한 방법: I/O를 블럭킹하거나 다른 종류의 오퍼레이션 블럭할때, 백그라운드 스레드는 이 오퍼레이션을 완료하기까지 깔끔하게 기다리는데 사용될 수 있다. 스레드의 이런 사용은 앱의 전반적인 설계를 증진하고 블럭된 호출 trivial을 다룰 수 있게 한다.

그러나 여러분의 코드를 단일 스레드의 관점에서 볼 때 이해했던 몇가지 가정이 다중 스레드가 실행있을때는 더이상 유효하지 않을 것이다.

각 스레드의 실행이 독립적으로 이루어지고, 데이터공유가 없는 이상적인 세계라면 단일 스레드에서 실행되는 코드처럼 그렇게까지 복잡하지는 않을 것이다. 그러나 보통의 경우처럼 같은 데이터에 동작하는 다중 스레드를 가진다면 이런 자료구조에 접근을 규제해야하고, 이 데이터에대한 모든 오퍼레이션이 다른 스레드의 오퍼레이션과 원치않은 인터렉션이 없도록 만드는 방법이 필요할 것이다.

동시성 프로그래밍은 그 언어나 운영체제로부터 추가적인 보증이 필요한데, 여러 스레드가 동시에 접근하려할때 변수("자원")는 어떻게 행동할지 명시적인 지정이 필요하다.

이런 언어는 메모리모델(Memory Model)을 정의해야한다. 메모리모델의 기본 진술서(basic statments)에는 동시성 스레드에서 어떻게 행동할지 명시적으로 지정해놓은 규칙들을 담아야하고. 메모리가 어떻게 공유될 수 있고 어던 종류의 메모리 접근이 유효한지 정의해야한다.

덕분에 사용자는 예상한대로 동작하는 언어를 가지게 되며, 컴파일러는 메모리 모델에 정의된 것만 반영하여 최적화를 수행할 것이라는 점을 우리는 알 것이다.

너무 엄격한 모델은 컴파일러가 발전할 것을 제안하기 때문에 메모리 모델을 정의하는 것은 언어의 발전에서 정교하게 해야한다. 독창적인 최적화는 메모리모델에서 과거의 결정에 유효하지 않을 수도 있다.

메모리모델을 정의하는 예시이다.
  • 어떤 언어의 진술서에는 atomic이 고려될 수 있는데, 어떤 스레드도 부분적인 결과를 결과를 내지 않는 완전한 곳에서만 오퍼레이션을 실행시킬 수 있다. 예를들어 필수적으로 변수들이 atomic하게 초기화될 수 있는지 없는지 알아야한다.
  • 공유된 변수를 어떻게 스레드에의해 다룰지, 디폴트로 캐싱을 할지, 특정 언어 변경자로 캐시 동작에 영향을 줄 수 있게 할지
  • 크리티컬 섹션(critical section, 공유된 자원에서 동작하는 코드 영역)에 접근을 표시하고 규제하는데 사용되는 동시성 연산자가 있다. 예로서 이것은 한번에 특정 한 코드 패스를 따르기위해 한 스레드만 허용한다.
이제 여러분의 프로그램의 동시성 사용 이야기로 돌아가자.

동시성을 올바르게 다루기위해 여러분 프로그램에서 크리티컬 섹션을 판단해야하고, 다른 스레드간에 공유된 데이터의 접근을 규제하기위해 동시성 프리미티브나 동시성을 인지하는 자료구조를 사용해야 할 것이다.

코드나 자료구조의 이런 영역에 접근 규칙을 만들면 또다른 문제들을 만들게된다. 모든 스레드가 실행하여 공유된 데이터를 수정할 기회를 제공하는 것이 바라는 결과겠지만, 어떤 환경아래 어떤 것들은 아예 실행되지 않을 수도 있고, 그 데이터는 예상하지 못했던 방법으로 변경될지도 모른다.

당신은 추가적인 과제들을 직면하게 될 것이고 어떤 일반적인 문제들과 함께 작업해야 할 것이다.
  • Race Conditions: 같은 데이터에 실행되는 여러 스레드(예를들면 동시에 읽기, 쓰기를 하는)는 오퍼레이션 시리즈의 실행 결과를 예측하기 힘들거나 스레드 실행 순서에 따라 다른 결과가 나올 수 있다.
  • Resources Contention: 다른 작업들을 실행시킬 수 있는 멀티 스레드가 같은 자원에 접근하려고하면, 요청했던 자원을 안전하게 얻는데 시간이 더 많이 요구될 것이다. 여러분이 필요한 자원을 얻는데 이런 지연은 기대하지 않았던 동작이 되버리거나, 아니면 이런 자원 접근을 규제하는 구조를 짜야한다.
  • Deadlocks: 여러 스레드에서 자원에 락을 걸었는데 서로 그 락이 풀리기를 기다리게된다. 이 스레드 그룹은 영원히 실행을 블락시킨다.
  • Starvation: 한 스레드가 절때 특정 순서에서 자원들을 얻지 못할 수 있다. 다양한 이유가 필요하며 영원히 성공하지 못할 자원 취득을 계속해서 시도한다.
  • Priority Inversion: 시스템에의해 할당된 우선순위 전환으로 높은 우선순위의 스레드가 필요로하는 자원을 낮은 우선순위의 스레드가 계속해서 취득하고 있을 수 있다.
  • Non-determinism과 Fairness: 우리는 언제 어느때의 순서에따라 스레드가 공유된 자원을 취득할 수 있을지 가정할 수 없다. 이런 지연은 우선순위를 결정할 수 없고 경쟁의 양에 크게 영향을 받는다. 그러나 크리티컬 섹션을 보호하는데 사용되는 동시성 프리미티브는 공평하게 만들어지거나, 공평을 지원하게 만들 수도 있다(used to guard a critical section can also be built to be fair or to support fairness). 기다리고 있는 모든 스레드가 크리티컬 섹션에 접근할 수 있게 보장하면서, 요청했던 명령을 침해하지 않는다.

언어 게런티
당장 스위프트 자체가 동시성과 관련된 기능을 가지고 있지 않더라도, 스위프트는 프로퍼티를 어떻게 접근할지와 관련된 몇가지 게런티를 제공한다.

예를들어 전역변수는 atomic하게 초기화되므로, 여러 스레드가 한 전역변수를 동시에 초기화하려는 상황을 직접 처리하지 않아도 되고, 초기화가 여전히 진행중일때 누군가 부분적으로 초기화된 모습을 볼 걱정을 할 필요가 없다.

아래에 싱글톤 구현을 이야기할때 이 동작에대해 다시 생각해볼 것이다.

그러나 레이지 프로퍼티(lazy property) 초기화는 atomic하게 수행되지 않는다는 것을 꼭 기억해줘야한다. 게다가 스위프트는 이제 이것을 바꾸기위한 지시자나 변경자를 제공하지 않는다.

클래스 프로퍼티에 접근도 atomic이 아니다. 만약 그렇게 만들어야 한다면, 락이나 다른 비슷한 메커니즘을 사용해서 직접 독점적 접근을 구현해야한다.

스레드
Foundation은 Thread 클래스를 제공하는데, 이 클래스는 내부적으로 pthread를 기반으로 하며, 새로운 스레드를 생성하고 클로저를 실행시키는데 사용할 수 있다.

Thread 클래스의 detachNewThreadSelector:toTarget:withObject: 메소드를 이용하여 스레드를. 생성하거나, 커스텀 Thread 클래스를 선언하고 main() 메소드를 오버리아딩하여 새로운 스레드를 만들 수도 있다.
class MyThread : Thread {
   override func main() {
       print("Thread started, sleep for 2 seconds...")
       sleep(2)
       print("Done sleeping, exiting thread")
   }
}
그러나 iOS10과 macOS Sierra부터는 마침내 모든 플랫폼에서 스레드가 실행시킬 클로저를 생성자뒤에 붙여 새로운 스레드를 생성할 수 있다. 이 글의 모든 예제는 기본 Thread 클래스를 확장한 것이므로 다른 OS에서 테스트해보지 않아도 된다.
var t = Thread {
   print("Started!")
}

t.stackSize = 1024 * 16
t.start()              //Time needed to spawn a thread around 100us
우리가 직접 시작시키기위해 필요한 스레드 인스턴스를 만들어보자. 부가적인 단계로 새로운 스레드를 위한 맞춤형 스택 크기도 지정할 수 있다.

exit()를 호출하여 갑자기 스레드를 중단시킬 수 있지만, 현재 작업들을 깔끔하게 끝낼 기회를 잃어버리므로 절때로 추천하지 않는다. 필요에따라 중단 로직을 스스로 구현하거나, cancel() 메소드를 사용하고 스레드가 자연스럽게 현재 작업을 끝내기전에 중단을 요청을 받았는지 메인 클로저 내에서 알기위해 isCancelled 프로퍼티를 확인할 수 있다.

동기화 프리미티브
공유된 데이터를 변경하고 싶은 다른 스레드들이 있을때는, 데이터 오염이나 결정되지 않은 동작을 막기위해 반드시 이런 스레드들을 어떤 방법으로 동기화해주어야한다.

스레드 동기화에 기본적으로 사용되는 것은 락(lock), 세마포어(semaphore), 모니터(monitor)이다.

Foundation은 이 모든것을 제공한다.

곧 보게 될것인데, 이런 구성들을 구현하는 클래스들(그렇다 모두 참조 타입이다)은 스위프트 3에서 접두를 빼진 않았지만 다음 스위프트 배포판 중 하나에서 빠질 수 있다.

NSLock
NSLock은 Foundation이 제공하는 락(lock)의 기본 타입이다.

스레드가 이 오브젝트에 락을 걸려고하면 두가지 일이 일어날 수 있다. 이전 스레드가 락을 걸지 않았다면 이 스레드는 락을 취득할 것이다. 혹은 락이 이미 걸려있다면 락을 건 소유자가 락을 풀때까지 스레드는 실행을 블락하고 기다릴 것이다. 즉 락은 한번에 한번에 한 스레드만 취득할 수 있는 오브젝트이며 이것이 크리티컬 섹션 접근을 완벽하게 감시할 수 있게 만들어준다.

NSLock과 Foundation의 다른 락은 공평하지 않다(unfair). 스레드의 시리즈가 락을 취득하기위해 기다릴때 원래 락은 시도했던 순서대로 락을 취득하지 않을 것이다.

스레드 경쟁이 커지는 경우에는 실행 순서를 예상할 수 없다. 많은 스레드가 자원을 취득하려 할때, 여러분의 스레드는 starvation을 겪을 수 있고, 아무리 기다려도 절때 락을 취득할 수 없을 수도 있다(혹은 적절한 시간안에 취득할 수 없을 것이다).

경쟁 없이 락을 취득하는데 필요한 시간은 100ns로 예상할 수 있겠지만, 하나 이상의 스레드가 락이 걸린 자원을 취득하려고 할때, 그 시간은 급격하게 증가한다. 따라서 성능의 관점에서 볼때 락은 자원 할당을 다루기에 최고의 해결책은 아니다.

두 스레드가 있는 예제를 보자. 락을 취득될 순서가 정해져있지 않으므로 T1이 한 row에 두번 락을 취득하는 일이 일어날 수 있다(일반적인 상황은 아니다).
let lock = NSLock()
class LThread : Thread {
   var id:Int = 0

   convenience init(id: Int) {
       self.init()
       self.id = id
   }

   override func main() {
       lock.lock()
       print(String(id)+" acquired lock.")
       lock.unlock()
       if lock.try() {
           print(String(id)+" acquired lock again.")
           lock.unlock()
       } else {  // If already locked move along.
           print(String(id)+" couldn't acquire lock.")
       }

       print(String(id)+" exiting.")




    }
}

var t1 = LThread(id:1)
var t2 = LThread(id:2)
t1.start()
t2.start()
락을 사용하기로 했을때 한가지 경고하고 싶은게 있다. 나중에 동시성 이슈를 디버깅해야할 것이다. 항상 어떤 종류의 자료구조 범위 안으로 락 사용을 제한하려 하고, 여러분의 코드베이스 여러곳에서 하나의 락 오브젝트를 직접 참조하지 않도록 노력해야한다.

동시성 문제를 디버깅하는동안, 여러분의 코드 어느 부분이 락을 잡고있는지 계속 추적해가면서 여러 함수들의 로컬 상태를 기억하는것보다는 몇가지 입장 지점으로 동기화된 자료구조의 상태를 확인하는 것이 더 좋은 방법이다. 남은 글로 가서(go the extra mile) 여러분의 동시적인 코드 구조를 잘 짜자.

NSRecursiveLock
재귀적인 락(recursive lock)은 이미 락을 건 스레드에서 여러번 락을 취득할 수 있는데, 재귀함수나 시퀀스에서 동일한 락을 확인하는 여러 함수를 호출할 시 유용하다. 이것은 기본 NSLock과는 함께 동작하지 않을 수 있다.
let rlock = NSRecursiveLock()

class RThread : Thread {

   override func main() {
       rlock.lock()
       print("Thread acquired lock")
       callMe()
       rlock.unlock()
       print("Exiting main")
   }

   func callMe() {
       rlock.lock()
       print("Thread acquired lock")
       rlock.unlock()
       print("Exiting callMe")
   }
}

var tr = RThread()
tr.start()

NSConditionLock
조건락(condition lock)은 더 복잡한 락 설정(소비자-생산자 시나리오)을 지원하는데, 각자 독립적으로 락과 언락될 수 있도록 추가적인 하위락을 제공한다.

하나의 전역의 락(특정 조건에 상관없이 락을 건다)도 사용할 수 있으며 원래의 NSLock처럼 동작한다.

공유하는 정수를 보호하는 락 예제를 보자. 소비자는 출력하고 생산자는 화면에 나타날 때마다 업데이트한다.
let NO_DATA = 1
let GOT_DATA = 2
let clock = NSConditionLock(condition: NO_DATA)
var SharedInt = 0

class ProducerThread : Thread {

   override func main() {
       for i in 0..<5 {
            clock.lock(whenCondition: NO_DATA) //Acquire the lock when NO_DATA
            //If we don't have to wait for consumers we could have just done clock.lock()
            SharedInt = i
           clock.unlock(withCondition: GOT_DATA) //Unlock and set as GOT_DATA
        }
   }
}

class ConsumerThread : Thread {

   override func main() {
       for i in 0..<5 {
            clock.lock(whenCondition: GOT_DATA) //Acquire the lock when GOT_DATA
            print(i)
            clock.unlock(withCondition: NO_DATA) //Unlock and set as NO_DATA
        }
   }
}

let pt = ProducerThread()
let ct = ConsumerThread()
ct.start()
pt.start()
락을 만들때 시작 조건을 지정해주어야하는데, 정수로 표현한다.

lock(whenCondition:) 메소드는 조건이 만족될때 락을 취득하거나 다른 스레드가 unlock(withCondition:)을 이용해서 값을 세팅할때까지 기다릴 것이다.

기본 락보다 조금 개선된 점은 좀 더 복잡한 시나리오를 만들 수 있게 해준다는 점이다.

NSCondition
조건락과 헷갈리지 말자. 한 조건(condition)은 발생 조건을 기다리기위한 명확한 방법을 제공한다.

락을 취득했던 스레드가 동작을 수행하는데 필요한 추가조건이 아직 만족되지 않았다면, 잠시 잡아두고 조건이 만족할때 작업을 계속하게 하는 방법이 필요하다.

끊임없이나 주기적으로 조건을 확인하도록 구현할 수도 있지만(busy waiting), 그렇게하면 스레드가 잡고있는 락에서 무슨일이 일어날까? 조건이 만족할때 다시 이들을 취득하길 바라면서 기다리거나 풀어주는 동안 잡아둬야 하는가(Should we keep them while we wait or release them hoping that we’ll be able to acquire them again when the condition is met)?

조건은 이 문제에대해 명확한 솔루션을 제공한다. 한번 취득한 스레드는 그 조건에대해 기다리고 있는 목록에 들어갈 수 있고, 한번 깨어난 다른 스레드는 조건이 만족했다고 신호를 보낸다.

예제를 보자.
let cond = NSCondition()
var available = false
var SharedString = ""

class WriterThread : Thread {
       override func main() {
       for _ in 0..<5 {
           cond.lock()
           SharedString = "😅"
           available = true
           cond.signal() // Notify and wake up the waiting thread/s
           cond.unlock()
       }
   }
}

class PrinterThread : Thread {
       override func main() {
       for _ in 0..<5 { //Just do it 5 times
           cond.lock()
           while(!available) {  //Protect from spurious signals
                cond.wait()
           }
           print(SharedString)
           SharedString = ""
           available = false
           cond.unlock()
       }
   }
}

let writet = WriterThread()
let printt = PrinterThread()
printt.start()
writet.start()

NSDistributedLock
분산된 락(distributed lock)은 지금까지 우리가 봤던 것과는 꽤 다르고, 이것이 자주 필요해보이진 않는다.

이것은 여러 어플리케이션 간에 공유되도록 만들어졌고 파일시스템 출입을 지원한다. 이 파일시스템은 이것을 취득해야하는 모든 앱이 분명하게 접근할 수 있어야 할것이다.

이런 종류의 락은 try() 메소드를 사용하여 취득될 수 있는데, 이 논-블락킹 메소드는 락이 취득되있는지 아닌지를 알려주는 boolean을 바로 반환한다. 락을 얻으려면 보통 한번 이상 시도해야 할것이다. 직접 실행시키거나 적절한 딜레이를 두고 연속적으로 시도할 수 있다.

분산될 락은 보통 unlock() 메소드를 사용하여 락을 푼다.

아래 기본 예제를 보자.
var dlock = NSDistributedLock(path: "/tmp/MYAPP.lock")

if let dlock = dlock {
   var acquired = false
   while(!acquired) {
       print("Trying to acquire the lock...")
       usleep(1000)
       acquired = dlock.try()
   }

   // Do something...
   dlock.unlock()
}

OSAtomic 어디있는가(Where Art Thou)?
OSAtomic가 제공하는 것과 비슷한 atomic 오퍼레이션들은 기존의 락 로직을 사용하지 않고 변수를 set, get, compare-and set 할 수 있게 해주는 간단한 오퍼레이션이다. 이들은 CPU의 특정 기능(종종 네이티브 atomic 인스트럭션)을 이용하여 앞에서 설명했던 락보다 더 좋은 성능을 낸다.

동시성을 다루는데 필요한 오버헤드가 최소한으로 줄기 때문에, 동시성 자료구조를 만들때 극도로 편리하다.

OSAtomic은 macOS 10.12부터 디프리케이트되었고 리눅스에서는 아예 사용할 수 없으나, 이것처럼 스위프트의 유용한 익스텐션을 사용한 오픈소스 프로젝트나 이것은 비슷한 기능을 제공한다.

synchronized 블럭에서
Objective-C에서 했던것처럼 @synchronized 블럭은 스위프트에서는 만들 수가 없는데, 동일한 키워드가 없다.

다윈에서는 objc_sync_enter(OBJ)objc_sync_exit(OBJ)를 직접 사용하여, 비슷한 어떤것을 준비할 수 있고, 내부적으로 @synchronized와 비슷하게 동작하는 @objc 오브젝트 모니터도 있다. 그러나 별로 의미는 없고, 이런것이 필요할때는 간단하게 락을 쓰는게 더 낫다.

그리고 Dispatch Queues를 설명할때 보게될 것인데, 동기화 호출을 수행하는 작은 코드로 이 기능을 큐로 이용할 수 있다.
var count: Int {
   queue.sync {self.count}
}

이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

GCD: Grand Central Dispatch
이 API에 친숙하지 않은 이들을 위해 Grand Central Dispatch(GCD)를 설명하자면, 이것은 큐 기반 API로 작업자 풀(worker pools)에서 클로저를 실행할 수 있게 해준ㄷ.

실행되야하는 작업을 담은 클로저는 이것을 실행시킨 큐에 담을 수 있는데, 큐의 구성 옵션에따라 순차적으로 할지, 병렬적으로 할지 정한 스레드 시리즈를 이용한다. 그러나 큐의 타입에 상관없이 작업은 항상 먼저 들어온 것이 먼저 나가는(FIFO, First-in First-out) 순서로 시작될 것이다. 즉, 작업은 항상 들어온 순서대로 시작할 것이다. 완료 순서는 각 작업의 지속시간에따라 다르다.

이것은 상대적으로 현대의 언어 런타임이 동시성을 처리할때 일반적으로 발견할 수 있는 패턴이다. 스레드 풀(thread pool)은 일련의 프리 스레드(free thread)나 연결되지 않은 스레드보다 더 쉽게 관리하고 조사하며 컨트롤 할 수 있는 방법이다.

스위프트 3에서 GCD API는 조금 바뀌었다. SE-0088는 설계를 현대화시키고 더 객체지향적으로 만들었다.

Dispatch Queues
GCD는 커스텀 큐를 생성할 수 있을 뿐만 아니라, 몇몇 미리 선언된 시스템 큐에 접근하게도 해준다.

일련의 기본 큐(이 큐는 여러분의 클로저를 차례로 실행시킬 것이다)를 생성하기 위해서는 큐를 식별하는데 쓰이는 문자열 레이블을 제공해야하며, 스택 트레이스(stack trace)에서 큐의 소유자를 간단히 추적하기위해 이 레이블은 도메인 앞부분을 뒤집어 사용하는 것을 추천한다.
let serialQueue = DispatchQueue(label: "com.uraimo.Serial1")  //attributes: .serial
let concurrentQueue = DispatchQueue(label: "com.uraimo.Concurrent1", attributes: .concurrent)
우리가 생성한 두번째 큐는 동시에 된다. 큐는 작업이 실행될때 스레드 풀에 있는 모든 사용가능한 스레드를 사용할 것이다. 이 경우에 실행 순서는 예측할 수 없고, 여러분의 큐를 추가한 순서와 어떤 방법으로도 연관시켜 완료 순서를 가정해서는 안된다.

디폴트 큐는 DispatchQueue 오브젝트에서 찾아볼 수 있다.
let mainQueue = DispatchQueue.main
let globalDefault = DispatchQueue.global()
main 큐는 순차적인 메인 큐인데, 이 큐는 iOS나 macOS에서 그래픽적인 어플리케이션을 위한 메인 이벤트 루프를 처리한다. 이 큐는 이벤트에 응답하고 사용자 인터페이스를 업데이트한다. 우리가 알고 있듯, 사용자 인터페이스에서 일어나는 모든 변경은 이 큐에서 실행되고, 이 스레드에서 오퍼레이션이 길어지면 둔감한 사용자 인터페이스로 렌더링하게 될 것이다.

이 런타임은 다른 프로퍼티로 다른 전역의 큐에 접근할 수 있게 해주는데, Quality of Service(QoS)라는 파라미터로 식별된다.

높은 우선순위부터 낮은 우선순위까지 다양한 우선순위가 DispatchQoS 클래스에 정의되있다.
  • .userInteractive
  • .userInitiated
  • .default
  • .utility
  • .background
  • .unspecified
모바일기기에서 저전력모드로 해놨을때 베터리양이 작으면 background 큐는 중단될 것이다. 이 점을 기억하자.

특정 디폴트 큐를 얻기위해 원하는 우선순위를 지정하는 global(qos:) 게터를 사용하자.
let backgroundQueue = DispatchQueue.global(qos: .background)
동일한 우선순위 명시는 커스텀 큐를 생성할때 다른 속성들과 함께(혹은 없이) 사용될 수 있다.
let serialQueueHighPriority = DispatchQueue(label: "com.uraimo.SerialH", qos: .userInteractive)

Queue 사용하기
클로저 형태의 작업들은 두가지 방법으로 큐에 담긴다. 동기적으로 sync 메소드를 사용하거나 비동기적으로 async 메소드를 사용할 수 있다.

전자를 사용하면 sync 호출은 블락되며 즉 이 클로저가 완료될 때 sync 메소드를 호출할 수 있는 반면(클로저가 끝날때까지 기다려야할 때는 유용하지만, 더 나은 방법이 있다), 후자는 클로저를 큐에 넣고 완료되는대로 계속해서 실행할 수 있게 해준다.

짧은 예제를 보자.
globalDefault.async {
   print("Async on MainQ, first?")
}

globalDefault.sync {
   print("Sync in MainQ, second?")
}
예제처럼 여러 디스패치 호출은 중첩될 수 있는데, background(낮은 우선순위)가 끝나고 사용자 인터페이스를 갱신하는 오퍼레이션이다.
DispatchQueue.global(qos: .background).async {
    // Some background work here
    DispatchQueue.main.async {
        // It's time to update the UI
        print("UI updated on main queue")
   }
}
클로저는 지정된 지연 이후에 실행될 수도 있는데, 스위프트 3은 마침내 더 편리한 방법으로 지정할 수 있게 되었다. .seconds(Int), milliseconds(Int), microseconds, nanoseconds(Int)의 네가지 시간 단위를 사용하여 인터벌을 구상할 수 있는 DispatchTimeInterval 열거형을 사용할 수 있다. 이 열거형으로 원하는 인터벌을 지정할 수 있다.

나중에 실행될 클로저의 스케줄을 짜기위해, 시간 인터벌과함께 asyncAfter(deadline:excute:) 메소드를 사용하자.
globalDefault.asyncAfter(deadline: .now() + .seconds(5)) {
   print("After 5 seconds")
}
같은 클로저를 여러번 순회하면서 실행시켜야 한다면(dispatch_apply를 사용하는 것과 같이), concurrentPerform(iterations:execute:) 메소드를 사용할 수 있다. 그러나 현재 큐의 맥락중에 가능하다면 이 클로저는 동시에 실행된다는 것을 알고 있어야 한다. 그러니 동시성을 지원하는 큐에서동작하는 sync 호출이나 async 호출에 항상 concurrentPerform(iterations:execute:) 호출을 넣어야 함을 기억하자.
globalDefault.sync {
     DispatchQueue.concurrentPerform(iterations: 5) {
       print("\($0) times")
   }
}
큐가 정상적으로 생성되는데 클로저를 처리할 준비를 하는동안 ,필요한 것을 할 수 있게 설정할 수 있다.
let inactiveQueue = DispatchQueue(label: "com.uraimo.inactiveQueue", attributes: [.concurrent, .initiallyInactive])
inactiveQueue.async {
   print("Done!")
}

print("Not yet...")
inactiveQueue.activate()
print("Gone!")
하나 이상의 속성을 지정하는 것은 처음이지만, 여러분도 볼 수 있듯, 필요하면 배열로 여러 속성들을 추가할 수 있다.

작업의 실행은 DispatchObject에서 상속한 메소드로 중단되거나 잠시 멈출 수 있다.
inactiveQueue.suspend()
inactiveQueue.resume()
setTarget(queue:)는 비활성 큐의 우선순위 구성에만 쓰이는데(이것을 활성 큐에 사용하면 크레쉬가 난다), 이 메소드도 사용할 수 있다. 이 메소드를 호출하면, 큐의 우선순위가 파라미터로 주어진 큐의 우선순위와 같아진다. 

Barriers
특정 큐에 (서로다른 지속시관과 함께) 일련의 클로저를 넣었지만, 이제 당신은 이전의 비동기 작업이 모두 완료되고나서 그 작업을 실행시키고 싶다. 이를위해 barriers를 사용할 수 있다.

우리가 이전에 만들었던 동시적 큐에 5개의 테스크(1초에서 5초까지 sleep하는 테스크)를 추가하고, 다른 작업이 완료되면 뭔가를 출력하기위해 barrier를 사용해보자. 마지막 async 호출에 DispatchWorkItemFlags.barrier를 지정할 것이다.
globalDefault.sync {
    DispatchQueue.concurrentPerform(iterations: 5) { (id:Int) in
       sleep(UInt32(id) + 1)
       print("Async on globalDefault, 5 times: " + String(id))
    }
}

globalDefault.async (flags: .barrier) {
   print("All 5 concurrent tasks completed")
}

싱글톤과 Dispatch_once
이미 알고 있을지도 모르겠지만 스위프트 3은 dispatch_once와 동일한 함수가 없다. 이 함수는 싱글톤을 스레드-세이프하게 만드는데 많이 사용되었었다.

운좋게도 스위프트는 전역변수가 atomic하게 초기화됨을 보장한다. 그리고 상수가 한번 초기화되고나면 값을 바꿀 수 없다는 점을 생각해본다면, 이 두 프로퍼티를 이용한 전역 상수는 싱글톤을 쉽게 구현하기에 좋은 후보자가 될 것이다.
final class Singleton {

   public static let sharedInstance: Singleton = Singleton()

   private init() { }

   // ...
}
우리 클래스를 final로 선언하여 상속할 수 없게 만들고, 지정된 생성자를 private으로 만들어서 이 오브젝트의 인스턴스를 추가로 생성하지 못하게 만든다. public static 상수는 싱글톤에서 들어가는 부분이고, 이것은 단 하나의 공유된 인스턴스를 찾는데 사용될 것이다.

한번만 실행될 코드블럭을 만들때도 똑같이하면 된다.
func runMe() {
   struct Inner {
       staticlet i: () = {
           print("Once!")
       }()
   }
   Inner.i
}

runMe()
runMe() // Constant already initialized
runMe() // Constant already initialized
가독성은 떨어지지만 동작은 한다. 한번만 쓰이기위한 코드이면 수용할 수 있는 구현이다.

그러나 만약 그 기능과 dispatch_once API를 정확하게 복제해야한다면, 동기화된 블럭 섹션에서 한 익스텐션으로 표현하듯 처음부터 구현해야한다(But if we need to replicate exactly the functionality and API of dispatch_once we need to implement it from scratch, as described in the synchronized blocks section with an extension).
import Foundation public extension DispatchQueue {

   private static var onceTokens = [Int] ()
   private static var internalQueue = DispatchQueue(label: "dispatchqueue.once")

   public class func once(token: Int, closure: (Void) -> Void) {
       internalQueue.sync {
           if onceTokens.contains(token) {
               return
           } else {
               onceTokens.append(token)
           }
           closure()
       }
   }
}

let t = 1
DispatchQueue.once(token: t) {
   print("only once!")
}
DispatchQueue.once(token: t) {
   print("Two times!?")
}
DispatchQueue.once(token: t) {
   print("Three times!!?")
}
예상한대로 세개중 첫번째 클로저만 실제 호출될 것이다.

대신 여러분의 플랫폼에서 쓸 수 있다면 objc_sync_enterobjc_sync_exit를 사용하여 약간 더 좋은 성능을 낼 수도 있다.
import Foundation

public extension DispatchQueue {

   private static var _onceTokens = [Int] ()

   public class func once(token: Int, closure: (Void) -> Void) {
       objc_sync_enter(self);
       defer { objc_sync_exit(self) }
       if _onceTokens.contains(token) {
           return
       } else {
           _onceTokens.append(token)
       }
       closure()
   }
}

Dispatch Groups
(다른 큐에 추가할지라도) 여러 테스크를 가지고 있으면서 그것들의 완료를 기다린다면 dispatch group으로 그룹화할 수 있다.

예제를 보면 syncasync 호출로 한 테스크를 직접 특정 그룹에 추가할 수 있다.
let mygroup = DispatchGroup()

for i in 0..<5 {
   globalDefault.async(group: mygroup) {
       sleep(UInt32(i))
       print("Group async on globalDefault:" + String(i))
   }
}
이 테스크는 globalDefault에서 실행되지만 mygroup 완료를위한 핸들러를 등록할 수 있다. 이 핸들러는 모든 테스크가 완료되면 우리가 원하는 큐에서 한 클로저를 실행시킬 것이다. wait() 메소드는 블럭킹 지연(blocking wait)을 실행시키는데 사용할 수 있다.
print("Waiting for completion...")
mygroup.notify(queue: globalDefault) {
   print("Notify received, done waiting.")
}
mygroup.wait()
print("Done waiting.")
그룹으로 작업을 추적할 수 있는 또다른 방법이 있다. 큐에서 호출할 때 그룹을 지정하는 것 대신, 직전 그룹을 들어가고(enter) 나오도록(leave) 설정하는 것이다.
for i in 0..<5 {
   mygroup.enter()
   sleep(UInt32(i))
   print("Group sync on MAINQ:" + String(i))
   mygroup.leave()
}

Dispatch Work Items
클로저는 큐에서 실행될 작업을 지정해주는 일만 하는것이 아니라, 그것의 실행 상태를 계속 추적할 수 있도록 컨테이너 타입도 필요하다. 이를위해 DispatchWorkItem이 사용된다. 클로저를 받는 모든 메소드는 work item 종류를 가진다.

work item은 스레드풀의 큐에의해 실행될 클로저를 캡슐화하는데, perform() 메소드를 호출한다.
let workItem = DispatchWorkItem {
   print("Done!")
}
workItem.perform()
그리고 WorkItems은 다른 유용한 메소드를 제공한다. notify는 특정 큐에서 완료될때 클로저를 실행시킨다.
workItem.notify(queue: DispatchQueue.main) {
   print("Notify on Main Queue!")
}
defaultQueue.async(execute: workItem)
클로저가 실행될때까지 기다리게 할 수도 있고, cancel 메소드를 써서 큐가 실행하려하기 전에 제거하라고 알릴 수도 있다.
print("Waiting for work item...")
workItem.wait()
print("Done waiting.")
workItem.cancel()
여기서 한가지 알아야할 중요한 사실은 wait()는 완료를 기다리기위해 현재 스래드를 블럭하지 않고 큐 안에서 바로 이전의 work item들의 우선순위를 올려, 이 특정 item을 가능한 빨리 완료시키려 한다는 점이다.

Dispatch Semaphores
dispatch semaphore는 락이다. 이것은 카운터의 현재 값에따라 하나 이상의 스레드가 락을 취득할 수 있게 해준다.

카운터(세마포어를 취득할대마다 감소시킴)가 0이 될때 스레드는 세마포어를 wait한다.

세마포어에 접근하기위한 슬롯은 대기중인 스레드 호출 signal에대해 열리는데, 이 signal은 카운터를 증가시킨다.

예제를 보자.
let sem = DispatchSemaphore(value: 2)
// The semaphore will be held by groups of two pool threads
globalDefault.sync {
   DispatchQueue.concurrentPerform(iterations: 10) { (id:Int) in
       sem.wait(timeout: DispatchTime.distantFuture)
       sleep(1)
       print(String(id) + " acquired semaphore.")
       sem.signal()
   }
}

Dispatch Assertions
스위프트 3은 현재 실행 맥락안에서 assertion을 실행시킬 수 있는 함수를 소개했다. 이것은 원했던 큐에서 크롤저가 실행되고 있는지 확인할 수 있게 한다. 우리는 DispatchPredicate 열거형의 3가지 case로 만들 수 있다. .onQuquq는 특정 큐에 있는지 확인한다. .notOnQueue는 그 반대를 확인한다. .onQueueAsBarrier는 현재 클로저나 work item이 한 큐의 barrier로 동작하고 있는지 확인한다.
dispatchPrecondition(condition: .notOnQueue(mainQueue))
dispatchPrecondition(condition: .onQueue(queue))
이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

Dispatch Sources
dispatch sources는 이벤트 핸들러를 사용하는 이벤트(커널 시그널이나 시스템, 파일, 소켓)와 관련된 시스템단의 비동기 이벤트를 편리하게 처리할 수 있게 해준다.

몇가지 종류의 Dispatch Sources를 사용할 수 있는데, 아래처럼 묶을 수 있다.
  • Timer Dispatch Sources: 시간내에 혹은 주기적인 이벤트에서 특정 시점에 이벤트를 만드는데 사용된다(DispatchSouceTimer).
  • Signal Dispatch Souces: UNIX 시그널을 다루는데 사용된다(DispatchSourceSignal).
  • Memory Dispatch Sources: 메모리 사용 상태 관련 알림을 등록하는데 사용된다(DispatchSourceMemoryPressure).
  • Discriptor Dispatch Sources: 파일이나 소캣 관련 여러 이벤트를 등록하는데 사용된다(DispatchSourceFileSystemObject, DispatchSourceRead, DispatchSourceWrite).
  • Process Dispatch Sources: 어떤 이벤트에대해 그들의 실행 상태 관련 외부 프로세스를 모니터링하는데 사용된다(DispatchSourceProcess)
  • Mach related Dispatch Source: Mach 커널의 IPC 기능과 관련된 이벤트를 처리하는데 사용된다(DispatchSourceMachReceive, DispatchSourceMachSend).
그리고 필요시 여러분만의 Dispatch Sources를 만들 수도 있다. 모든 Dispatch Sources는 DispatchSourceProtocol을 따르며, 이 프로토콜은 핸들러를 등록하고 Dispatch Sources의 활성 상태를 수정하는데 필요한 기본 오퍼레이션을 정의한다.

이 오브젝트를 어떻게 사용하는지에대한 이해를 돕기위해 DispatchSourceTimer 예제를 한번 보자.

Sources는 DispatchSource가 제공하는 실용적인 메소드로 생성할 수 있다. 이 단편코드에서는 makeTimerSource를 사용하여 핸들러 실행하는데 사용하고 싶은 dispatch 큐를 지정할 것이다.

Timer Sources는 다른 파라미터가 없으므로 큐만 지정하면 source를 생성할 수 있다. 곧 보겠지만, 여러 이벤트를 처리할 수 있는 dispatch source는 항상 이벤트 식별자를 지정해야 할 것이다.
let t = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
t.setEventHandler{ print("!") }
t.scheduleOneshot(deadline: .now() + .seconds(5), leeway: .nanoseconds(0))
t.activate()
source가 생성되면 setEventHandler(closure:)로 이벤트 핸들러를 등록하고, 다른 설정이 필요없으면 active()로 dispatch source를 켜자(이전에 libDispatch는 resume()을 사용해야 했다).

Timer Sources는 오브젝트가 전달할 이벤트에 어떤 종류의 시간을 설정할지에대한 추가적인 단계가 필요하다. 위 예제에서 우리는 엄격한 시간제한 등록 후 5초 딜레이될 이벤트를 정의하고 있다.

이벤트를 전달하기위한 오브젝트도 설정할 수 있는데, Timer 오브젝트로 하는 것처럼 가능하다.
t.scheduleRepeating(deadline: .now(), interval: .seconds(5), leeway: .seconds(1))
dispatch source로 작업을 끝내고나서 이벤트 전달을 완전히 멈추려면, 이밴트 소스를 중지시키는 cancel()을 호출하고, 핸들러를 설정했다면 취소를 호출한 뒤, 핸들러를 등록 해제하는것처럼 최종 정리 작업을 실행한다.
t.cancel()
handleRead() 함수는 소켓에 들어오는 데이터 버퍼에서 새로운 바이트를 사용할 수 있게 되면 전용 큐에서 호출하게 될 것이다. Kitura도 버퍼로된 쓰기를 위해 WriteSource를 사용하는데 쓰기 속도(pace)를 효율적으로 맞추기위해 dispatch source 이벤트를 사용하며, 소켓 채널이 보낼 준비가 되는대로 새로운 바이트를 쓴다. I/O를 할때 read/write dispatch sources는 보통 *NIX 플랫폼에서 사용하는 저수준 API보다 더 좋은 고수준의 대안이 될 수 있다.

나머지 source 타입들도 비슷하게 동작한다. 사용할 수 있는 모든 항목들은 libDispatch의 문서에서 확인할 수 있지만, 그 중 Mach source나 memory pressure source 같은 몇몇은 다윈 플랫폼에서만 동작한다는 것을 기억하자.

Operations과 OperationQueues
Operation Queues에대해 간단히 이야기해보자. 이것은 GCD의 상위에 탑재된 추가 API이다. 이것은 동시적인 큐와 모델 테스크를 오퍼레이션으로 사용하고, 취소하기도 쉬우며, 다른 오퍼레이션이 완료됨에따라 그들의 실행을 가질 수 있다.

Operations은 실행 순서를 정하는 우선순위를 가질 수 있다. 그리고 이것은 OperationQueues에 추가되어 비동기적으로 실행된다.

기본적인 예제를 보자.
var queue = OperationQueue()
queue.name = "My Custom Queue"
queue.maxConcurrentOperationCount = 2

var mainqueue = OperationQueue.main //Refers to the queue of the main thread

queue.addOperation{
   print("Op1")
}
queue.addOperation{
   print("Op2")
}
Block Operation 오브젝트를 생성하여 큐에 넣기전에 설절할 수도 있고, 필요하면 이런 종류의 오퍼레이션에 한개 이상의 클로저를 넣을 수도 있다.

target과 selector로 오퍼레이션을 생성하는 NSInvocationOperation은 스위프트에서는 사용할 수 없다.
var op3 = BlockOperation(block: {
   print("Op3")
})
op3.queuePriority = .veryHigh
op3.completionBlock = {
   if op3.isCancelled {
       print("Someone cancelled me.")
   }
   print("Completed Op3")
}

var op4 = BlockOperation {
   print("Op4 always after Op3")
   OperationQueue.main.addOperation {
       print("I'm on main queue!")
   }
}
Operation은 우선순위(priority)와 완료 클로저를 가질 수 있다. 여기서 이 클로저는 메인 클로저가 완료되면 실행될 것이다.

op4에서 op3까지 의존성(dependency)을 추가할 수 있으므로 op4op3의 완료를 기다렸다가 실행될 것이다.
op4.addDependency(op3)

queue.addOperation(op4)  // op3 will complete before op4, always
queue.addOperation(op3)
removeDependency(operation:)으로 의존성을 제거할 수 있고, 이 의존성들은 public으로 접근할 수 있는 dependencies 배열에 담겨있다.

한 오퍼레이션의 현재 상태는 특정 프로퍼티를 이용해서 알 수 있다.
op3.isReady       //Ready for execution?
op3.isExecuting   //Executing now?
op3.isFinished    //Finished naturally or cancelled?
op3.isCancelled    //Manually cancelled?
cancelAllOperations 메소드를 호출하여 한 큐에 있는 모든 오퍼레이션 프레젠트를 취소할 수 있다. 이 메소드는 큐에 남아있는 오퍼레이션의 isCancelled 플레그를 on으로 설정한다. 한 오퍼레이션을 취소할때는 그것의 cancel 메소드를 호출하면 된다.
queue.cancelAllOperations()

op3.cancel()
어느 큐에서 실행될지 스케줄이 잡힌 뒤에 오퍼레이션이 취소되었다면, 그 오퍼레이션 안에서 실행을 스킵하기위해 isCancelled 프로퍼티를 확인할 것을 추천한다.

이제 마침내 당신은 한 오퍼레이션 큐에서 새로운 오퍼레이션 실행도 멈출 수 있게 되었다(현재 실행되고 있는 오퍼레이션에는 영향을 주지 않는다).
queue.isSuspended = true
이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

마지막 생각들
이 글은 오늘날 스위프트에서 사용할 수 있는 외브 프레임워크를 이용하여, 동시성 관점에서 무엇을 할 수 있는지 좋은 정리를 제공할 것이다.

Part2는 언어 기능의 관점에서 외부 라이브러리에 의존하지 않고 "네이티브한" 동시성을 처리하는 것에 대해 초점을 맞출 것이다. 오늘날 이미 있는 몇가지 오픈소스 구현의 도움을 받아 몇가지 흥미로운 패러다임을 설명할 것이다.

이 두개의 글이 동시성의 세계에 입문하기 좋게 만들어주고, 이것이 스위프트 5(희망하길)에서 고려되기 시작할때 swift-evolution 메일링 리스트의 토론을 이해하고 참여하는데 도움이 될 것이다.

동시성이나 스위프트에 더 흥미로운 자료는 Cocoa With Love 블로그에서 확인할 수 있다.

이 글이 마음에 든다면 나에게 윗해달라!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


요즘 앱에서 멀티 스레딩이나 컨커런시(concurrency)는 거의 필수이다.. 그리고 동시성을 관리해주는 시스템단 라이브러리인 Grand Central Dispatch는 iOS SDK에서 아직까진 다루기 까다롭고 친숙하지 않은 API를 제공해왔었다.

하지만 더이상은 아니다.

Swift3에서 Grand Central Dispatch의 문법과 사용법이 많이 개선 되었다. 이 글은 그것의 새로운 사용법을 빠르게 훑어볼 것이다.

dispatch_async
이전에는 dispatch 메소드(동기적이든 비동기적이든)를 고른 뒤, 우리가 dispatch 하고싶은 작업을 큐에 넣었다. 새로 바뀐 GCD는 이 순서가 반대이다.  ― 큐를 먼저 고른 뒤에 dispatch 메소드를 적용한다.

일반적인 GCD 패턴은 글로벌 백그라운드 큐에서 작업을 수행하고, 작업이 끝나는대로 메인 큐에서 UI를 갱신하는 방식이다. 아래 코드는 새 API의 모습이다.


큐 속성들
이제 큐가 초기화 시점에서 속성을 받는다. 이것이 Swift OptionSet이며, 순차적vs동시, 메모리, 엑티비티 관리 옵션과 서비스 품질(.default, .userInteractive, .userInitiated, .utility and .background)과 같은 큐 옵션을 설정할 수 있다.

서비스의 품질(The quality of service)은 앞서 iOS8부터 디프리케이트(deprecated)된 이전 속성 대신으로 사용된다. 만약 예전 방식으로 큐를 사용하고 있었다면, 어떻게 바뀌었는지 확인해보기 바란다.

* DISPATCH_QUEUE_PRIORITY_HIGH: .userInitiated
* DISPATCH_QUEUE_PRIORITY_DEFAULT: .default
* DISPATCH_QUEUE_PRIORITY_LOW: .utility
* DISPATCH_QUEUE_PRIORITY_BACKGROUND: .background



메모리 엑티비티 관리 옵션은 올해(2016년) 애플OS를 릴리즈하면서 새로 나왔다. .initiallyInactive를 사용하여 비활성 상태에서 큐를 시작하게 한다던지, .autoreleaseInherit, .autoreleaseNever and .autoreleaseWorkItem를 사용해여 커스텀된 오토릴리즈를 설정할 수 있다.

Work items
큐들은 GCD에서만 Swift OptionSet을 필요로 하는게 아니다. 새로 바뀐 work item의 Swift 문법에서도 쓰인다.

한 work item은 이제 퀄리티나 서비스를 정의하고 (혹은) 초기화때 flags를 줄 수 있다. 이 둘다 선택적으로 가능하며 work item 실행시 영향을 준다. flags는 barrier, detached, assignCurrentContext, noQoS, inheritQoS, enforceQoS 옵션들이다.

dispatch_once
dispatch_once는 한번만 실해오디는 코드나 함수들을 초기화하는데 매우 유용했다.

Swift3에서는  dispatch_once가 디프리케이트 되었고 이것은 글로벌, 스태틱 변수와 상수를 사용하는 것으로 대체되었다.


dispatch_time_t
dispatch_time_t는 큐에서 사용할 수 있는 UInt64로 특정 시간을 변환하는 함수이다. 새로 바뀐 GCD는 이것에대해 좀 더 친숙한 문법을 소개했다.(NSEC_PER_SEC여 안녕!) 아래 코드는 바뀐 dispatch의 예제이다:

.second는 DispatchTimeInterval에서 불려진 새 열겨형 중 하나이다. 이 열겨형은 카운트를 표현하기위한 값들을 갖는다. 현재 지원하는 것들이다:
* .seconds(Int)
* .milliseconds(Int)
* .microseconds(Int)
* .nanoseconds(Int)


dispatch_assert
또한 이번에 새로 발표한 애플OS에서는 dispatch precondition이 있다. 이것은 dispatch_assert를 대체하며, 이것은 코드가 실행되기 전에 스레드를 생각하고 있는지 아닌지 체크할 수 있다. 이것은 특히 UI를 업데이트하고 메인 큐에서 반드시 실행되야할 함수에 유용하게 쓰인다. 예제코드를 한번보자:


추가적인 자료들

여기 Swift3을 포함한 더 많은 GCD 개선에 관한 이야기들이 있지만, 아직 공식적인 문서는 완성되지 않고 있다. 더 심화된 자료들이다:
  • https://github.com/apple/swift-evolution/blob/master/proposals/0088-libdispatch-for-swift3.md
  • https://developer.apple.com/videos/play/wwdc2016/720/
  • https://github.com/apple/swift-corelibs-libdispatch



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원본 : http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1

위 링크의 강좌를 2포스트에 걸쳐서 번역을 진행할 것이다. 본 포스트는 위 원문 포스팅의 절반을 번역한 것이다.


Update note : Swift를 사용한 iOS8 기반의 Grand Central Dispatch tutorial 최신 튜토리얼이 있다!

비록 Grand Central Dispatch(혹은 줄여서 GCD)가 얼마동안 주변에 있어 왔다고 해서, 모두가 그것을 잘 아는건 아니다. 이것은 이렇게 이해될 수 있다; 동시에 하는것은 편법이고, C기반 API의 GCD는 Objective-C의 세계로 자연스럽게 뽀죡한 끝으로 푹 찌르는 것처럼 보인다. 깊이있게 두 파트의 시리즈를 가진 Grand Central Dispatch 강좌를 배워보자.

이 두 파트에서, 첫번째 강좌에서는 GCD가 뭘하는지와 GCD 기능 기반의 몇몇 예시를 보여줄것이다. 두번째 파트는 GCD가 가진 더 많은 기능적인 이점을 배울 것이다. 



GCD란?
GCD는 libdispatch를 위한 마케팅적인 이름이다, 애플 라이브러리는 iOS와 OSX의 멀티코어 하드웨어에서 코드 동시 실행을 위한 기능을 제공한다. 이것은 아래 이점들을 따른다:
  • GCD는 무거운 처리를 살짝 뒤로 미루고 백그라운드에서 처리할 수 있게 도와주므로써  당신 앱의 대응력을 높혀준다.
  • GCD는 쓰레드를 락 거는것 보다 더 쉬운 동시 실행 모델을 제공하고 동시 실행시 생기는 버그들을 피할 수 있게 도와준다.
  • GCD는 싱글톤으로써 일반적인 패턴의 높은 퍼포먼스와 함께 당신의 코드를 아름답게 만들어 줄 것이다.
이 강좌는 블록코딩과 GCD가 어떻게 동작하는지 잘 알고있다는 가정하에 진행한다. 만약 GCD를 처음 접한다면 Multithreading and Grand Central Dispatch on iOS for Beginners를 먼저 필수로 보고 오면 좋겠다.


GCD 용어
GCD를 이해하기위해, 여러분은 쓰레딩과 동시실행어대한 몇몇 개념을 명확하게 할 필요가 있다. 이것들은 둘다 모호하고 헷갈리는 것이지만 잠시 다시 GCD의 관점에서 그것들을 가볍게 생기시켜 봐야한다.
(역자 주 : 원문에서는 GCD 용어라고 해놓았지만, 구글에 "쓰레드 용어"로 검색하면 더 많은 정보를 얻을 수 있다)

Serial vs. Concurrent
이 용어는 작업들이 서로관에 연관이되어 실행되는 경우를 설명하는 용어이다. 연속적으로 실행되는 작업들은 항상 한번에 실행된다. 작업들은 한번에 동시에 실행되어야한다.

비록 이 용어가 넓은 응용프로그램이지만, 이 강의의 목적상 당신은 Objective-C block 작업 수행에만 초점을 맞출 수 있다. block이 뭔지 모른다고? 여기 이 강좌(How to Use Blocks in iOS 5 Tutorial)를 참고해라. 사실 당신은 또한 함수포인터와 함께 GCD를 사용할 수 있지만, 대부분의 케이스에서는 이것이 사용에 실질적이고 편법적이다. Block은 매우 편하다!



Synchronous vs. Asynchronous
GCD에서, 이 용어는 함수가 연관된 다른 작업을 끝났을때 함수는 실행하기위해 GCD에게 물어보는 것을 뜻한다. 동기화(synchronous) 함수는 명령된 일이 모두 끝난 후에 리턴한다.

반면 비동기화(asynchronous) 함수는 즉시 리턴하고, 일이 다 끝났다고 알려주지만 실제 일이 끝날때까지 기다리지는 않는다. 그러므로 비동기화 함수는 다음 함수에서 처리되는 실행의 현재 쓰레드 block이 아니다.

조심하자 - 현재 쓰레드의 동기화 함수 “blocks”을 읽을때나 함수가 “blocking”함수 이거나 blocking operation인경우 혼동하지 말자! blocks라는 동사는 어떻게 한 함수가 현재 쓰레드에 영향을 미치는지이고 명사 block과는 아무 연관이 없다. 명사 block는 Objective-C에서 이름없는 함수의 용어이고 GCD에 보내는 일들을 정의한다.

Critical Section
이것은 한번에 두 쓰레드가 도는 상황에서 반드시 동시에 실행되지 않는 코드 조각이다. 동시 프로세스에 의해 접근된다면, 이것은 코드가 (변수와 같은) 공유된 자원을 동시에 건드릴 수 있기 때문에 잘못된 값이 들어가거나 할 수 있다.

Race Condition
---

Deadlock
만약 그 두가지(혹은 그 이상)의 일이 서로의 처리가 끝나기를 기다리고 있을때 이것을 deadlock 되었다고 부른다(대부분의 쓰레드에서 나타날 수 있다). 첫번째 처리는 두번째 처리가 끝나기를 기다리기 때문에 끝날 수 없다. 그러나 두번째 처리 또한 첫번째 처리가 끝나기를 기다리기 때문에 두번째 처리도 끝날 수가 없을 것이다.

Thread Safe
쓰레드 세이프 코드는 여러 문제(데이터 오염, 크래쉬 등등)를 피하면서 멀티쓰레딩이나 동시처리로부터 안전하게 콜이 가능하다. 쓰레드 세이프가 되지 못한 코드는 한번에 한 콘텍스 안에서 동작한다. 쓰레드 세이프 코드 중 하나는 NSDictionary이다. 당신은 멀티 쓰레드 이슈 없이 저것을 이용할 수 있다. 반면 NSMutableDictionary는 쓰레드 세이프가 아니다. 이것은 오직 한번에 한 쓰레드안에서 접근할 수 있다.

Context Switch
context switch는 당신이 한 싱글 프로세스에서 다른 쓰레드를 실행하여 전환할 때의 저장 및 복구 실행 상태의 프로세스이다. 이 프로세스는 멀티쓰레딩하는 앱을 만들때 아주 일반적인 방법이지만, 이것은 추가적인 비용이 따른다.

Concurrency vs Parallelism

참고 한글 자료 : http://skyul.tistory.com/263
Concurrency와 parallelism은 종종 함께 거론된다. 그래서 짧게 그 두개를 비교하여 설명할 것이다.

concurrent 코드의 나눠진 부분은 “동시에” 실행될 수 있다. 그러나 그것이 어떻게 처리되는지는 시스템이 정하기 나름이다.

멀티코어 디바이스는 병렬적으로 같은 시간에 멀티 쓰레딩을 실행시키지만, 싱글코어 디바이스는 이것을 수행하기 위해 쓰레드를 실행시키고, 컨텍스 스위칭을 동작하며, 다른 쓰레드나 프로세스를 작동시킨다. 이것은 아래 그림처럼 마치 병렬적으로 수행되게 보이는데 충분히 빨리 수행된다.

비록 당신이 GCD에서 동시수행을 코드에 사용했었어도, 얼마나 병렬수행이 필요한지 GCD가 정하기 나름이다. 병력수행은 동시에 일어나는 것을 요구한다. 그러나 ‘동시’는 보장된 병렬을 제공하지 않는다.

중요한 포인트는 ‘동시’는 사실 구조에 관한 것이다. 당신이 GCD를 코드에 사용할 생각이 있을 때, 당신은 동시에 일어날 수 있는 일의 부분을 당신 코드 구조체에 노출한다. 게다가 한개는 반드시 동시에 일어나지 않는다. 만약 당신이 이 주제에대해 더 깊게 알고싶으면 this excellent talk by Rob Pike를 확인해보아라.

Queues

GCD는 코드의 블럭들을 다룰 수 있게
dispatch queues를 제공한다; 큐(queue)들은 당신이 GCD에 제공한 테스크를 관리하거나 FIFO 명령에 의한 테스크를 실행시킨다. 첫 테스크를 큐에 추가하는데 이점은 첫번째 테스크가 큐에서 시작되고 두번째로 추가된 테스크가 두번째에서 시작할것이며, 차례로 될 것이다. 

모든 dispatch queues는 당신이 멀티쓰레드에서 동시에 접근하려해도 스스로 쓰레드 세이프하다. 이러한 GCD의 이점은 어떻게해서 dispatch queues가 당신의 코드의 부분에서 쓰레드-세이프한 것을 제공하는지 이해할때 나타날 것이다. 이것의 핵심은 옳은 dispatch queue 종류와 옳은 dispatching function을 골라서 당신의 일의 queue에 보내기 위함이다.

이 섹션에서 특별한 GCD queue가 제공하는 두가지 종류의 dispatch queue를 보게 될 것이다, 그리고 GCD dispatching function과 함께
큐에 어떻게 추가하는지 알려주는 형상화한 예제를 돌려볼 것이다.

Serial Queues

serial queues에서 데스크는 한번에 한회만 실행된다, 각 테스크는 이전 실행되는 테스크가 끝나고나야지만이 시작된다. 뿐만아니라, 아래 그림과같이 우리는 한 블럭의 끝나는 점과 다음 블럭의 시작하는 점의 시간차이를 알지도 못한다.

이 테스크의 동작 타이밍은 GCD의 컨트롤 아래에 이루어진다; 당신이 알고 있는 GCD의 이점은 한번에 한번만 수행한다는것과 queue에 추가되어서 명령을 받으면 테스크를 실행한다는 것이다.

Seiral queue에서는 절때 동시에 두 테스크가 실행될 수 없으나, 동시실행에서 같은 섹션을 접근할 위험은 없다; 이 테스크를 오직 수행한다는 점에서 race condition으로부터 섹션이 위험해지는것을 막는다. 그러므로 위험한 섹션에 접근 할 수 있는 유일한 방법은 dispatch queue에 테스크를 담아서 보내는 방법이다, 그리고 위험한 섹션이 안전하다는 것을 검증받을 수 있다.



Concurrent Queues

concurrent queues에 테스크들은 추가하라는 명령에서 시작하는 보장을 가지고... 그것은 당신이 완전히 보장됬다는 것을 의미한다! 요소들은 어떤 명령들도 끝낼 수 있고 당신이 다음 블럭이 언제 시작될건지 모르거나 많은 블럭이 얼마동안 시간을 잡아먹으면서 동작하는지 몰라도된다. 이것이 GCD의 모든것이다.

아래 그래프는 GCD에서 4개의 동시수행을 동작하는 테스트 샘플이다.


Block 1, 2, 3 모두가 어떻게하면 빨리 연이어서 실행될지 주목하라, Block0이 시작된 후 블럭1이 시작되는 동안 일어날 것이다. 또한 Block3은 Block2 다음에 시작되지만 Block2보다 빨리 끝난다.

block이 언제 시작되는지의 결정은 완전히 GCD에서 한다. 만약 block의 실행 시간이 다른것과 겹칠때, 다른 코어에서 실행시킬지, 한개만 사용할것인지 혹은 코드를 다른 block에 콘텍스 스위칭을 할지 정하는건 GCD가 하기 나름이다.

단지 흥미로운 점은 GCD가 각자 다른 queue 타입으로부터 정해서 적어도 5가지의 각 queue들을 제공한다는 점이다.


Queue Types

처음으로, 이 시스템은 당신이 main queue라고 알고있는 특별한 종류의 큐를 제공한다. 다른 queue들과 같이, 이 큐에서도 한번에 테스크를 수행한다. 그러나 이것은 모든 테스크를 메인 스레드에서 수행할 것이라는 뜻이며, 당신의 UI를 유일하게 업데이트 할 수 있는 스레드이기도하다. 이 queue는 UIView에 메세지를 보내거나 노티피케이션을 보내는 것에 사용되어야하는 유일한 queue이다.

시스템은 또한 여러 동시 queues를 제공한다. 이것은 우리가 알고있는 Global Dispatch Queues라고 불리는 놈이다. 이것은 다른 우선순위를 가진 4개의 global queue가 있다: backgroundlowdefault, and high

마지막으로, 당신은 커스텀화된 종류나 동시 queues 또한 생성할 수 있다. 이 말은 당신이 적어도 5개의 queue들을 마음대로 생성 소멸시킬 수 있다는 뜻이다: main queue, 4개의 global dispatch queues, 추가로 당신이 커스텀화시켜 만든 queues 까지!

그리고 dispatch queue들의 큰 그림이 있다!

GCD의 예술적인 면은 queue에 당신의 일을 적당한 queue dispatching function을 골라 보내는 역할을 한다는 것이다. 이것을 경험하기 가장 좋은 방법은 권장해놓은 길을 따라서 예제를 실행해보는 것이다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,