[번역글] Concurrent Programming: APIs and Challenges

This posting is a Korean translation of “Objc.io”‘s contents. Please contact me if I need to remove it. Thank you.

Original article: Concurrent Programming: APIs and Challenges

 

동시성(Concurrency)라는 개념은 여러 과업(tasks)이 같은 시간에 처리되는 것을 뜻한다. 이것은 싱글 CPU 코어 환경에서는 시간 분할 처리 방식이나, 멀티 CPU 코어 환경에서 진정한 병렬 처리 방식과 같이 구현될 수 있다.

OSX와 iOS는 병행 프로그래밍(Concurrent programming)을 가능케 하는 다양한 API를 제공한다. 각각 API는 다 다른 능력과 한계를 가지고 있으며, 여러 종류의 과업에 알맞게 설계되었다. 그리고 각 API는 다 다른 추상화(Abstraction) 정도의 레벨로 구현되어 있다. 하위 레벨 정도의 조작이 가능하지만 그에 따른 책임도 따른다.

병렬 프로그래밍은 난해한 문제들과 상황들이 존재하는 매우 까다로운 주제이며, GCD나 NSOperationQueue와 같은 API를 사용하면서 쉽게 까먹을 수 있는 개념이다. 먼저 이 글에서는 OSX와 iOS의 병렬처리 관련된 API를 전체적으로 살펴 볼 것이며, 전부터 내려오던 병렬 프로그래밍의 어려운 과제들에 대해서 깊이 알아볼 것이다.

 

OS X와 iOS의 병렬 API

애플의 모바일과 데스크톱 OS는 병렬 프로그래밍을 위한 동일한 API를 제공한다. 이 글에서는 pthread와 NSThread, GCD, NSOperationQueue, 그리고 NSRunLoop을 살펴 볼 것이다. 기술적으로 run loop이 소개 된다는 것이 이상할지 모른다. 왜냐하면 진정한 병렬처리와 관계가 없기 때문이다. 하지만 이 주제와 함께 자세히 알아볼 필요가 있다고 생각한다.

우리는 먼저 하위 레벨 API를 시작으로 점점 상위 레벨 API를 살펴 볼 것이다. 이렇게 작성한 이유는 상위 API들은 하위 API들 위에 만들어진 것이기 때문이다. 아무튼 실제 개발에서는 지금과 같은 방식이 아닌 상위 API를 먼저 검토하고 사용해야 할 것이다. 왜냐하면 코드를 쉽게 만들고 병렬 처리 모델을 더 심플하게 유지 할 수 있기 때문이다.

만약 우리가 왜 상위 레벨 추상화와 매우 심플한 병렬 처리 코드를 계속적으로 추천하는지 궁금 할 것이다. 그렇다면 이글의 두번째 부분과 Peter Steinberger가 작성한 쓰레드 세이프관련 포스트를 읽어보길 추천한다.

 

쓰레드

쓰레드는 운영체재의 스케쥴러에 의해서 독립적으로 스케쥴이 잡히는 프로세스의 하위 유닛이다. 가상으로 GCD와 NSOperationQueue의 모든 병렬 처리 API는 쓰레드 위해서 만들어 졌다.

하나 이상의 쓰레드는 싱글 CPU 코어에서도 실행될 수 있다. (최소한 느끼기에 동시에 실행되고 있다고 느끼게 해준다.) 운영체제는 각 쓰레드에게 짧은 시간을 할당해 주며 계산을 하는데, 사용자는 여러 쓰레드가 동시에 실행 되는 처럼 느낄 것이다. 만약 하나 이상의 CPU 코어가 탑재 되어 있다면 실제로 병렬 처리를 하여 특정 과업의 전체 시간을 줄여 줄 것이다.

멀티 CPU 코어에서 당신의 코드와 당신이 사용하고 있는 프레임워크 코드가 실행 스케쥴을 얼마나 잡는지 확인하고 싶다면, XCode의 Instruments의 CPU strategy를 사용하된다.

마음에 염두하고 있어야 할 것은 우리에게는 언제 어디서 작성한 코드가 스케쥴 잡힐지 모른다는 것이다. 그리고 또한 언제 얼마나 길게 할당을 위해 기달려야 자신의 차례가 오는지도 알 수 없다. 이런 종류의 쓰레드 스케쥴링은 매우 강력한 테크닉이다. 하지만 매우 난이도 높은 작업이다. 우리는 앞으로 이것을 조사해 볼 것이다.

복잡도에 대한 내용은 잠시 뒤로하고, POSIX 쓰레드 API를 사용하든지 Objective-C로 랩핑되어 있는 NSThread를 사용하여 자신만의 쓰레드를 만들수 있다. 다음은 pthread를 사용하여, 100만의 숫자 가운데서 가장 작은 수와 가장 큰수를 찾는 간단한 샘플 코드이다. 4개의 쓰레드가 병렬 처리하면서 찾을 것이다. 코드를 살펴 보면 왜 pthread를 직접 쓰지 말아야 할 지 알게 될 것이다.

 

  1. #import <pthread.h>
  2.  
  3. struct threadInfo {
  4.     uint32_t * inputValues;
  5.     size_t count;
  6. };
  7.  
  8. struct threadResult {
  9.     uint32_t min;
  10.     uint32_t max;
  11. };
  12.  
  13. void * findMinAndMax(void *arg)
  14. {
  15.     struct threadInfo const * const info = (struct threadInfo *) arg;
  16.     uint32_t min = UINT32_MAX;
  17.     uint32_t max = 0;
  18.     for (size_t i = 0; i < info->count; ++i) {
  19.         uint32_t v = info->inputValues[i];
  20.         min = MIN(min, v);
  21.         max = MAX(max, v);
  22.     }
  23.     free(arg);
  24.     struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
  25.     result->min = min;
  26.     result->max = max;
  27.     return result;
  28. }
  29.  
  30. int main(int argc, const char * argv[])
  31. {
  32.     size_t const count = 1000000;
  33.     uint32_t inputValues[count];
  34.  
  35.     // Fill input values with random numbers:
  36.     for (size_t i = 0; i < count; ++i) {
  37.         inputValues[i] = arc4random();
  38.     }
  39.  
  40.     // Spawn 4 threads to find the minimum and maximum:
  41.     size_t const threadCount = 4;
  42.     pthread_t tid[threadCount];
  43.     for (size_t i = 0; i < threadCount; ++i) {
  44.         struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));
  45.         size_t offset = (count / threadCount) * i;
  46.         info->inputValues = inputValues + offset;
  47.         info->count = MIN(count – offset, count / threadCount);
  48.         int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
  49.         NSCAssert(err == 0, @"pthread_create() failed: %d", err);
  50.     }
  51.     // Wait for the threads to exit:
  52.     struct threadResult * results[threadCount];
  53.     for (size_t i = 0; i < threadCount; ++i) {
  54.         int err = pthread_join(tid[i], (void **) &(results[i]));
  55.         NSCAssert(err == 0, @"pthread_join() failed: %d", err);
  56.     }
  57.     // Find the min and max:
  58.     uint32_t min = UINT32_MAX;
  59.     uint32_t max = 0;
  60.     for (size_t i = 0; i < threadCount; ++i) {
  61.         min = MIN(min, results[i]->min);
  62.         max = MAX(max, results[i]->max);
  63.         free(results[i]);
  64.         results[i] = NULL;
  65.     }
  66.  
  67.     NSLog(@"min = %u", min);
  68.     NSLog(@"max = %u", max);
  69.     return 0;
  70. }

 

NSThread는 pthread를 랩핑한 Objective-C 클래스이다. 이렇게 랩핑을 한 것이 코코아 환경에서 더 익숙해 보인다. 예를 들어, 백그라운드에서 은닉화하고 싶은 코드를 쓰레드로 작성하고 싶다면, NSThread를 상속 받아서 작성하면 된다.

&nbsp

  1. @interface FindMinMaxThread : NSThread
  2. @property (nonatomic) NSUInteger min;
  3. @property (nonatomic) NSUInteger max;
  4. - (instancetype)initWithNumbers:(NSArray *)numbers;
  5. @end
  6.  
  7. @implementation FindMinMaxThread {
  8.     NSArray *_numbers;
  9. }
  10.  
  11. - (instancetype)initWithNumbers:(NSArray *)numbers
  12. {
  13.     self = [super init];
  14.     if (self) {
  15.         _numbers = numbers;
  16.     }
  17.     return self;
  18. }
  19.  
  20. - (void)main
  21. {
  22.     NSUInteger min;
  23.     NSUInteger max;
  24.     // process the data
  25.     self.min = min;
  26.     self.max = max;
  27. }
  28. @end

 

새로운 쓰레드를 시작하려면 쓰레드 객체를 만들어서 다음과 같이 start 메서드를 호출해야 한다.

  1. NSMutableSet *threads = [NSMutableSet set];
  2. NSUInteger numberCount = self.numbers.count;
  3. NSUInteger threadCount = 4;
  4. for (NSUInteger i = 0; i < threadCount; i++) {
  5.     NSUInteger offset = (numberCount / threadCount) * i;
  6.     NSUInteger count = MIN(numberCount – offset, numberCount / threadCount);
  7.     NSRange range = NSMakeRange(offset, count);
  8.     NSArray *subset = [self.numbers subarrayWithRange:range];
  9.     FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];
  10.     [threads addObject:thread];
  11.     [thread start];
  12. }

 

먼저 결과를 평가하기 전에 우리가 새로 작성한 쓰레드의 모든 코드가 끝나고 나서 thread 객체의 isFinished 프로퍼티를 알고 싶다면, 관심이 있는 독자들은 한번 직접 짜보길 권한다. 여기에서 진짜 말하고 싶었던 부분은 쓰레드를 직접 사용해 본다는 부분이다. pthread를 사용하든지, NSThread의 API를 사용하는 것은 우리 멘탈 모델에 적합하지 않는 비교적 시대에 뒤떨어지는 경험일지 모르지만 말이다.

마음에 직접적으로 떠오르는 한 가지 문제는, 만약 당신의 코드나 프레임워크 코드가 자신들의 쓰레드를 만들어 작동 한다면, 활성화 되는 쓰레드의 숫자가 기하급수적으로 늘어난다는 것이다. 해당 이슈는 큰 프로젝트에서 자주 발생하는 문제이다. 예들 들어, CPU 코어가 8개가 있는 환경에서 이점을 챙기기 위해 쓰레드 8개를 생성해서 사용한다고 치자, 각 쓰레드에서 위 로직이 들어 있는 프레임워크를 사용하게 되면, 결국 짧은 시간내에 수 백개나 되는 쓰레드를 만들어 낼 것이다. 각 코드들은 자신들의 책임을 다하여 작동할 것인데, 그럼에도 불구하고 결과는 좋은 모습이 아니다. 쓰레드를 잘 사용하기는 쉽지 않다. 각 쓰레드는 메모리와 커널 자원과 연결 되어 있기 때문이다.

다음으로, 우리는 큐 기반의 병령처리 관련 API를 살펴 볼 것이다. GCD와 NSOperationQueue인데, 이것들은 집중적으로 쓰레드 풀를 관리하여 해당 문제점들을 완화해 준다.

Grand Central Dispatch

늘어나는 사용자 디바이스의 CPU 코어 갯수의 이점을 개발자들이 누릴 수 있도록 애플은 Grand Central Dispatch를 OSX 10.6과 iOS4에서 부터 제공하기 시작했다. 하위 레벨의 병렬처리 API인 GCD에 대해서 이 글에서 더 자세히 알아 볼 것이다.

GCD를 사용하면 이제 더 이상 쓰레드를 직접적으로 상대하지 않아도 된다. 대신에 코드 블럭을 큐에 넣으면 밑 물에서 GCD가 쓰레드 풀을 관리 한다. GCD는 어떤 특정 코드 블럭의 쓰레드를 실행할지 정하고, 시스템의 가용 리소스에 따라서 쓰레드들을 관리한다. 쓰레드는 중앙 통제적으로 관리 되어지기 때문에 쓰레드 때문에 생기는 많은 문제점들을 줄어들게 된다.

또 다른 이점으로는 어플리케이션 개발자 입장에서는 복잡한 쓰레드 개념보다 큐 개념을 가지고 개발할 수 있게 되었다는 점이다. 새로운 병렬처리 개념은 기존 방식에 비해 쉽게 작업할 수 있다.

GCD는 다섯 종류의 큐를 제공한다. 메인 쓰레드위에서 도는 메인 큐, 세 개의 우선순위가 다른 백그라운드 큐, 그리고 I/O 처리같은 작업을 할 수 있는 보다 더 낮은 우선순위를 가지고 있는 백그라운드 큐이다. 더 나아가, 순차적 혹은 비동기식 커스텀 큐를 만들 수도 있다. 이렇게 강력한 추상화를 지닌 커스텀 큐는 특정한 하나의 글로벌 큐에 끼어 들어가게 되어 해당 쓰레드 풀에서 연관되게 된다.

Thread Pool

처음 다른 우선순위로 여러가지 큐를 만들어 사용한다는 것은 직관적으로 보일지 모른다. 하지만 우리는 어떤 경우에든지 여러분이 디폴트 우선순위가 지정되어 있는 큐를 최대한 사용하라고 권하고 싶다. 공유자원을 접근하는 과업을 작업하다 보면, 실행고자 하는 과업을 각 다른 우선순위와 함께 큐에 스케쥴링한다는 것이 예상하지 못한 작동을 불러이르킨다는 것을 알게 될 것이다. 또한 낮은 우선순위 과업이 높은 우선순위 과업을 가로막는 일이 잦아져, 프로그램 전체가 점점 느려지다가 멈추게 만들기도 한다. 이런 현상을 우선순위 전도(Priority Inversion)라고 부른다. 이 글 뒷부분에서 더 자세히 다룰 것이다.

비록 GCD는 하위 레벨 C API이지만, 사용하는데 있어서 직관적이다. 디스페칭 블럭을 GCD의 큐에 적용하므로써 병렬처리 프로그램에서 발생하는 어려움들과 알고 있는 문제점들을 생각나지 않게 된다. 병렬 프로그래밍에서 발생 가능한 문제들에 대한 내용을 서술한, 해당 글의 병렬처리 프로그래밍이 처한 난관들을 꼭 읽어보기 바란다. 더 나아가 다음 포스팅에서는 GCD를 통해 이러한 어려운 난관들을 어떻게 해결할 수 있는지 알아 볼 것이다.

 

Operation Queue

Operation 큐는 GCD API 기반으로 만들어진 추상화 된 코코아의 큐 모델이다. GCD는 Operation 큐에 비해 낮은 레벨에서 컨트롤을 할 수 있게 해주지만, Operation 큐는 어플리케이션 개발자들에게 보다 안전하고 편리한 기능을 제공한다.

NSOperationQueue는 메인 큐(main queue)와 커스텀 큐(custom queue)로 구분 할 수 있다. 메인 큐는 메인 쓰레드 위에서 돌며. 커스텀 큐는 백그라운드에서 진행이 된다. 어떤 경우든, 해당 큐에서 진행되는 과업들은 NSOperation을 상속받은 서브 클래스들이다.

You can define your own operations in two ways: either by overriding main, or by overriding start. The former is very simple to do, but gives you less flexibility. In return, the state properties like isExecuting and isFinished are managed for you, simply by assuming that the operation is finished when main returns.

사용자는 main 메서드를 오버라이드 한다든지, start 메서드를 오버라이드하는 방식으로 두 가지 방법으로 자신만의 operation을 정의할 수 있다. 첫 번째 방식(main 메서드를 오버리이드 하는 방식)은 간단하지만 사용자가 할 수 있는 것이 제한 되어있다.

  1. @implementation YourOperation
  2.     – (void)main
  3.     {
  4.         // do your work here …
  5.     }
  6. @end

만약 좀 더 많은 컨트롤을 원하거나, 비동기식 작업을 해당 operation에서 실행하기 원한다면 start 메서드를 오버라이드하면 된다.

  1. @implementation YourOperation
  2.     – (void)start
  3.     {
  4.         self.isExecuting = YES;
  5.         self.isFinished = NO;
  6.         // start your work, which calls finished once it's done …
  7.     }
  8.  
  9.     – (void)finished
  10.     {
  11.         self.isExecuting = NO;
  12.         self.isFinished = YES;
  13.     }
  14. @end

만약 start 메서드를 오버라이드 해서 사용하고자 할때 주의 할 점은 operation의 상태를 수동으로 관리해야한다. Operation 큐가 큐 안에 있는 Operation을 선택하여 실행하기 위해서는 KVO 방식을 사용하여 상태 프로퍼티 값을 설정해주어야 한다. 그러므로 만약 디폴트 접근자를 사용하지 않고 있을 경우에는, 올바르게 해당 KVO 메세지를 보내고 있는지 주의해야 할 것이다.

Operation queue에서 긴 작업을 위해 제공하는 작업 취소하기 기능을 제대로 사용하기 위해서는 ‘isCancelled’ 프로퍼티를 주기적으로 확인해야 한다.

  1. - (void)main
  2. {
  3.     while (notDone && !self.isCancelled) {
  4.         // do your processing
  5.     }
  6. }

사용자가 Operation 큐를 정의만 해놓으면 큐에 넣고 사용하는 것은 매우 쉽다.

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  2. YourOperation *operation = [[YourOperation alloc] init];
  3. [queue  addOperation:operation];

반면, 클래스를 정의해서 사용하지 않아도 블럭을 큐에 삽입해서 작업 할 수 있다. 이 방식은 간편하다. 예를 들어 사용자가 한번만 사용할 작업이라면 다음 코드와 같이 큐에 넣어 사용하면 된다.

  1. [[NSOperationQueue mainQueue] addOperationWithBlock:^{
  2.     // do something…
  3. }];

비록 위 코드와 같이 블록을 사용하여 스케쥴링하면 매우 편리하지만, NSOperation을 상속 받아서 작업하는 방식은 디버깅에 매우 유용하다. 사용자가 만약 description 메서드를 오버라이드하면 현재 특정 큐 안에 있는 operations들을 쉽게 알수 있기 때문이다.

operation이나 블록을 스케쥴링하는데 있어서, GCD에서 제대로하기 쉽지 않은 몇가지 작업을 operation 큐에서 제공해주고 있다. 예를 들어 maxConcurrentOperationCount 프로퍼티를 사용하여 특정 큐에서 병렬처리 할 과업의 수를 관리하기에 용의하다. 시리얼 큐에 설정을 할 시 다른 작업과 격리시키기 위한 목적으로는 사용할 수 있다.

또 다른 편리한 기능으로는 우선순위에 따라 특정 큐의 작업들의 정렬하는 기능이다. 이 기능은 GCD의 큐의 우선순위와 다른 기능이다. 오로지 하나의 큐에 스케쥴 잡힌 과업들의 실행에 영향을 주는 기능이다. 만약 다섯 가지 우선순위를 넘어서, 실행 순서가 정해져 있는 작업을 해야 한다면, 작업 간에 의존도를 부여해 줌으로써 원하는 순서에 실행을 시킬수 있다.

  1. [intermediateOperation addDependency:operation1];
  2. [intermediateOperation addDependency:operation2];
  3. [finishedOperation addDependency:intermediateOperation];

이 코드는 intermediateOperation를 operation1과 operation2가 끝내고서 실행이 되도록 의존도를 설정해 주고 있으며, finishedOperation은 intermediateOperation이 끝나면 실행이 될 것이다. Operation 의존도 실행 순서를 잘 정의해 주는 대단히 강력한 매카니즘이다. 이것은 사용자가 의존하고 있는 operation이 끝나야지만 실행 할 수 있도록 보장해 주는 operation 그룹 같은 것을 만들수 있게 해준다.

추상화의 기본적 특성 때문에 operation 큐는 GCD API와 비교하면 성능이 약간 떨어진다는 점이 있다. 하지만 대부분의 경우에는 임팩트가 약하며, operation 큐를 선택 사용하는 것이 좋은 선택이라고 생각한다.

Run Loops

런 루프는 과업들을 병렬실행 해주지 않기 때문에 기술적으로 GCD나 Operation 큐와 같은 병렬처리 매카니즘은 아니다. 하지만 런 루프는 직접적으로 메인 operation 큐와 메인 dispatch에서 실행되는 과업들과 연관이 있으며, 비동기식 코드 실행에 관한 매카니즘을 제공한다.

런 루프는 GCD나 Operation 큐를 사용하는 것 보다 헐씬 더 쉽게 사용할 수 있다. 왜냐하면 사용자는 병렬처리 대한 복잡한 것을 고려하지 않아도 비동기식으로 일을 해낼수 있기 때문이다.

런 루프는 언제나 하나의 특정한 쓰레드에서 작동된다. 메인 쓰레드와 연관되어 있는 메인 런 루프는 UI, 이벤트, 타이머, 그리고 커널 이벤트를 처리하기 때문에 코코아와 코코아 터치 어플리케이션에 중심적인 역할을 하고 있다. 사용자가 타이머를 사용하여 스케쥴를 잡거나, NSURLConnection을 사용하거나, 혹은 performSelector:withObject:afterDelay: 메서드를 호출 하면, 비동기식 작업을 처리하는 런 루프가 사용된다.

런 루프를 사용할 때마다 기억해야 할 것은 런 루프는 여러가지 모드로 실행 할 수 있다는 것이다. 각 모드는 런 루프가 반응하게 될 이벤트들을 정의한다. 이 방식은 메인 런 루프에 있는 과업들을 넘어 임시적으로 특정 과업들에게 우선순위를 정할 수 있는 좋은 방식이다.

이것에 대한 iOS의 예로 스크롤링을 될 수 있다. 사용자가 스크롤링을 하는 동안 런 루프는 디폴트 모드로 작동하지 않는다. 그렇기 때문에 예를 들어 타이머는 반응하지 않을 것이다. 스크롤링이 끝나면 런 루프는 디폴트 모드에 반환하고 큐에 적소된 다음 이벤트를 실행하게 될 것이다. 만약 스크롤링 중에 타이머가 작동하기 원한다면 런 루프의 NSRunLoopCommonModes에 추가해야 할 것이다.

메인 쓰레드는 항상 메인 런 루프를 설정하고 실행한다. 다른 쓰레드는 기본적으로 런 루프가 설정되어 있지 않는다. 사용자는 다른 쓰레드에도 런 루프를 설정할 수 있지만, 이런 경우는 거의 드물 것이다. 대부분은 메인 런 루프를 사용하는 것이 쉽다. 만약 메인 쓰레드에서 오래 걸리는 과업을 실행하고 싶지 않다면, 메인 런 루프로 부터 다른 큐에 넣어서 작동하게 하면 될 것이다. 크리스가 쓴 블로그 포스트에서 좋은 예재를 소개할 것이다.

그래도 만약 다른 쓰레드에 런 루프를 설정하여 사용하고 싶다면, 하나 이상의 입력 소스를 추가하는 것을 잊지 말아야 할 것이다. 만약 런 루프가 입력 소스가 설정되어 있지 않으면, 매번 실행하고 할 때 갑자기 종류될 것이다.

병렬처리 프로그램이 직면한 어려움들

병렬처리 프로그래밍을 작성하는 데는 많은 어려움이 따른다. 아무리 기본적인 작업을 하려고 해도 상호작용하는 여러 과업들이 병렬처리 될때 각각의 다른 상태를 감독하는 것이 어려워진다. 문제들은 병렬처리 코드를 디버깅하기 어려운 결정되지 않은 방식으로 나타다는 것이다.

지금부터 소개할 것은 병렬처리 프로그래밍의 예견치 못한 작동에 대한 예이다. 1995년 NASA는 화성에 정찰기를 보냈다. 탐사선이 화성에 성공적으로 착륙한지 얼마 지나지 않아 미션은 거의 실패로 돌아갈 위기에 처했다. 화성 탐사선은 원인불명으로 계속적으로 재 부팅이 되기 시작한 것이다. 낮은 우선순위 작업이 높은 우선순위 작업을 지속적으로 블록킹하여 나타나는 ‘우선순위 역전’ 현상으로 어려움을 겪고 있는 것이었다. 우리는 더 자세한 ‘우선순위 역전’ 현상에 대해서는 알아 볼 것이다. 그러나 이 예가 말해주고 있는 것은 엄청난 자원과 뛰어난 엔지니어들에게도 생긴 똑같은 어려움들이 당신에게 여러가지 방식으로 다가올 것이라는 것을 말해주고 있다.

공유자원

멀티 쓰레드에서 발생하는 수 많은 문제들의 근본지는 공유자원에 접근이다. 여기에서 말하는 자원은 클래스의 프로퍼티나 객체, 혹은 보통 경우 메모리, 네트워크 기기, 파일 등이 될 수 있다. 멀티 쓰레드 사이에서 공유하는 모든 것이 잠재적인 문제점이라고 할 수 있다. 그리고 반드시 안전한 측정을 취함으로 이러한 문제점들을 피해야 할 것이다.

소개한 문제를 재현해보기 위해 카운터로 사용하는 정수 프로퍼티 방식으로 되어 있는 자원을 예로 들어 보자. A와 B라고 하는 두 개의 쓰레드가 카운터를 동시에 평행하게 돌아간다고 가정해 보자. 문제는 C나 오브젝티브 C의 작성된 코드는 그냥 하나의 CPU의 기계식 명렁어 아니라는 것이다. 카운터의 숫자를 하나 올리기 위해서는, 현재 카운터의 값을 메모리에서 부터 불러와야 할 것이고, 해당 값을 하나 올린 다음에 다시 메모리에 써야할 것이다.

두 쓰레드가 동시에 처리하려고 시도하는 것을 상상해보라. 예를 들어, 쓰레드 A와 쓰레드 B가 17인 카운터의 값을 메모리로 부터 읽었다고 가정해보자. 쓰레드 A가 카우터의 값을 하나 올린 다음 18를 메모리에 쓰고, 쓰레드 B도 값을 하나 올린 다음 18이라는 값을 메모리에 썼다고 생각해 보라. 이 해당되는 데이터는 두번 값을 올렸음에도 실질적으로 1 밖에 안올라가는 오류가 생긴 것이다.

Screen Shot 2015-10-06 at 11.15.10 PM

이런 문제를 경함 조건(race condition)이라고 부르며, 멀티 쓰레드 환경에서 특정 쓰레드가 작업이 끝나기도 전에 다른 쓰레드가 동시에 자원에 접근할 때 항상 발생한다. 이런 상황을 막기 위해서는 멀티 쓰레드는 상호 베타적인 방식으로 공유자원에 접근해야 한다.

현실에서는 현존하는 CPU들이 최적화라는 이유로 읽기-쓰기 순서를 계속적으로 바꾸기(Out of order execution) 때문에 예재 보다 헐씬 복잡하다.

상호 베타

상호 베타적 접근이라는 뜻은 특정 자원에 한번에 하나의 쓰레드만 접근한다는 것을 말한다. 이것을 보장하기 위해서는 공유 자원에 접근하고자 하는 쓰레드는 먼저 뮤텍스 락을 획득하는 것이 필요하다. 자신의 작업이 끝나면 락을 풀어줌으로써 다른 쓰레드가 접근할 수 있는 기회를 얻어야 한다.

Screen Shot 2015-10-06 at 11.26.27 PM

상호 베타적 접근을 보장하기 위해서 추가적으로, 락은 반드시 순서 바꾸기 실행(out-of-order execution)을 통해 생기는 문제점들을 처리해야 한다. 프로그램 명령에 의해 정의된 순서에 따라 CPU가 메모리에 접근하는 것만 의지 해서는 부족하다. 사이드 이팩트를 피하는 방법은 메모리 방벽(barrier)을 사용하는 것이다. 메모리 방벽을 설정하면 순서 바꾸기 실행이 방벽에서는 생기지 않는다.(Setting a memory barrier makes sure that no out-of-order execution takes place across the barrier.)

물론 뮤텍스 락를 구현하는 것 자체로 경합 조건을 피할수 있다. 간단한 일이 아니며 현존하는 CPU에 대한 특별조취를 사용하는 것이 요구된다. 더 자세한 내용을 알고 싶다면 데니엘의 낮은 수준의 병렬처리 테크닉에 대한 글를 읽어보기 바란다.

Objective-C는 프로퍼티 선언시 atomic이라는 키워드 사용을 통해 언어적 레벨의 락 기능을 지원하고 있다. 프로퍼티를 atomic으로 선언하는 것은 공유자원의 매 접근시도 시 락과 언락을 해주는 결과를 가져다 준다. 만약을 위해 모든 프로퍼티를 atomic으로 선언해 줄수도 있지만 항상 비싼 비용이 든다는 것을 생각해야 한다.

자원에 락을 건다는 것은 언제난 성능적 비용든다. 락을 걸고 푸는 것은 멀티코어 시스템에서 구현하기 쉽지 않은 경함조건을 없는 것이 필요하다. 또한 락을 걸면 쓰레드는 먼저 자원은 사용하고 있는 다른 쓰레드 때문에 기달려야 한다. 이런 경우에는 sleep에 빠지게 되고 사용하던 쓰레드의 작업이 끝나면 노티를 받게 된다. 이모든 처리는 비용이 비싸고 어렵다.

또 다른 종류의 락이 있다. 어떤 락은 매우 저렴하지만 자원 경쟁에 약하다. 어떤 락은 기본적으로 비용이 비싸지만 자원 경쟁에서 강력하다.

락을 걸고 푸는 것에는 장단점이 있다. 그러므로 critical 섹션에 자주 들락거리는 것은 삼가해야 할 것이다. 동시에, 긴 실행 코드로 인해 락을 오래 잡는 것은 자원을 쓰고자 하여 락을 걸길 원하는 다른 쓰레드의 일을 하지 못하게 할 수도 있다. 참 어려운 문제이다.

동시에 실행하도록 짜여진 코드는 쉽게 접할 수 있지만, 공유 자원을 락하는 방식이 정해져 있어서 실제로 결과적으로는 하나의 쓰레드만 활성화 되는 결과를 낳는다. 사용자는 CPU 코어가 그들의 코드를 언제 스케쥴 잡아 사용할지를 판단하기 쉽지 않다. XCode의 Instrument의 CPU strategy 뷰를 통해 CPU 코어 할당을 효율적으로 사용하고 있는지 알아 볼수는 있을 것이다.

Dead Locks

뮤텍스 락은 경합조건을 해결해주지만 동시에 데드락이라는 새로운 문제도 만들어 낸다. 데드락은 하나 이상의 쓰레드가 다른 쓰레드의 작업이 끝나길 서로 기다리면서 생기는 현상이다.

Screen Shot 2015-10-13 at 1.22.50 PM

두 값을 스와핑하는 다음 예제를 한번 살펴보자.

  1. void swap(A, B)
  2. {
  3.     lock(lockA);
  4.     lock(lockB);
  5.     int a = A;
  6.     int b = B;
  7.     A = b;
  8.     B = a;
  9.     unlock(lockB);
  10.     unlock(lockA);
  11. }

위 코드는 거의 항상 잘 돌 것이다. 하지만 쓰레드가 동시에 상대방의 값으로 호출할 때는 발생하게 된다.

  1. swap(X, Y); // thread 1
  2. swap(Y, X); // thread 2

위와 같이 샐행하면 데드락에 빠질 것이다. 쓰레드 1은 x에 락을 걸것이고, 쓰레드 2는 y에 락을 것 것이다. 이렇게 된다면 두 쓰레드는 서로의 락이 풀리길 기다릴 것이며, 영원히 끝나지 않을 것이다.

다시한번 말하지만, 공유자원을 많은 쓰레드에 공유하거나, 락을 더 많이 걸 수록, 데드락에 빠질 위험을 더 커진다. 그렇기 때문에 최대한 간단하게 코딩을 해야 할 것이며, 쓰레드 사이에 공유자원을 최소화 해야한다. 로우 레벨의 병렬처리 API에 관한 글도 꼭 읽어 보길 바란다.

Starvation

고려해야 하는 상황이 많다고 생각할지 모르지만 새로운 난제를 하나 더 소개하고자 한다. 공유자원을 락하는 것으로 인해 읽기-쓰기 문제를 초래할 수있다. 많은 경우에 자원을 읽기 위한 접근을 한번에 하나씩 하는 것은 낭비일지도 모른다. 그러므로 리소스에 쓰기 락이 없는 한 읽기 락을 허용하는 경우가 많다. 이럴 경우에는 한 쓰레드에서 쓰기위한 락을 걸기위해 읽기위한 락들이 풀리길 기다리다가 굶줄이는 경우가 생긴다.

이 문제점을 해결하기 위해서는 읽기-쓰기 락과 같은 간단한 방법보다는 현명한 솔루션이 필요하다. 예를 들어 writer preference를 주거나 읽기-복사하기-갱신하기 알고리즘(read-copy-update algorithm)을 사용한다. 데니엘이 그가 쓴 로우 레벨 병렬처리 기법에 관한 글을 통해 GCD를 사용한 독자/싱글 writer 패턴을 어떻게 구현하는지 알려 줄것 이다.

우선순위 역전현상

우리는 이 글을 초반부에 NASA의 화성 탐색선이 겪은 병렬처리 관련 문제로 시작하였다. 지금부터 우리는 왜 탐색선이 거의 실패로 돌아 갈 뻔했는지 알아 볼것이며, 왜 우선순위 역전현상이라 불리는 이 어려움을 우리도 똑같이 당할수 있는지 알아 볼 것이다.

우선순위 역전현상은 낮은 우선순위 과업이 높은 우선순위 과업의 실행을 막는 상황를 말하는데, 효율적 진행을 위해 우선순위가 역전되는 것을 말한다. GCD가 파일 I/O를 위한 큐를 포함해 백그라운드 큐를 다양한 우선순위를 가지고 제공되고 있는 지금 꼭 알아두어야 할 중요한 내용이라고 생각한다.

해당 이슈는 높은 우선순위 과업과 낮은 순위 과업이 공유자원을 함께 사용할 때 생길 수 있다. 낮은 우선순위 과업이 공유자원을 사용하기 위해 락을 걸었다면, 높은 우선순위의 과업이 공유자원을 사용하고자 할 때 아무런 거리낌 없이 사용할 수 있도록 락을 일찍 풀어야 한다. 낮은 우선순위 과업이 자원을 점유하고 있으므로 높은 우선순위 과업이 블럭당하면, 그 사이에 큐 안에서 실행 가능한 과업 중 가장 높은 중간정도 되는 우선순위 과업이 높은 우선순위 과업 보다 앞서 실행될 가능성을 열어주게 된다. 이때 중간 우선순위 과업이 낮은 우선순위 과업의 락을 풀는 것을 방해하여 높은 우선순위 과업은 계속 공유자원의 락이 풀리길 기다리게 되여 실행하지 못하게 된다.

Screen Shot 2015-10-13 at 1.35.50 PM

당신의 코드에서는 화성의 탐사선이 재부팅을 하는 것 만큼 심각한 상황을 초래하지 않을 수 있지만 우선순위 역전현상은 꽤 자주 발생하는 현상이다.

특별한 경우가 아니면 서로 다른 우선순위 큐를 사용하지 않길 권유한다. 가끔 높은 우선순위 과업이 낮은 우선순위 과업이 끝날 때 까지 기다리는 현상이 이럴날 수 있기 때문이다. GCD를 사용할 때, 디폴트 우선순위 큐를 사용하길 권한다. 만약 다른 우선순위 규를 사용하게 되면, 상황을 더 악화시킬 수 있다.

하나 이상의 큐를 사용할 때 각기 다른 우선순위를 설정하여 사용하는 것은 좋아 보이지만, 병렬처리 프로그래밍에서 ‘우선순위 역전현상’과 같은 과거의 경험을 통해 보면, 복잡성을 높이게 되며 예상할 수 없는 상황을 만들게 된다. 만약 앞으로 개발을 하면서 높은 우선순위 과업이 예상한 타이밍에 실행되지 않을 때 NASA의 전문 엔지니어도 경험한 ‘우선순위 역전현상’을 떠올리게 될 것이다.

마치며…

우리는 API가 아무리 사용하기 쉽게 보일지라도 해당 글을 통해 병렬처리 프로그래밍의 복잡성을 알려주길 원했고 그에 수반한 문제점을 알아 보길 원했다. 이러한 문제들은 가지고 오는 결과들은 어디가 문제인지 쉽게 파악하기 어렵고, 디버깅하기 조차 매우 어렵다.

반면에 병렬처리 기술은 멀티 코어 컴퓨팅의 이점을 사용할 수 있는 매우 강력한 도구이다. 성공의 열쇠는 병렬처리 모델을 최대한 간단하게 유지하여 락을 필요할 때만 걸라고 하는 것이다.

우리가 추천하는 안전한 방법은 메인 쓰레드에서 데이터를 뽑아내서 백그라운드 큐에서 실제 처리를 하고 결과를 다시 메인 쓰레드에 전달하라는 것이다. 이 방식은 락을 걸지 않게 되하여 실수를 줄일 수 있을 것이다.

[Multi-Thread 프로그래밍] Swift에서 GCD 사용하기

몇해 전 Apple은 NSOperation과 동시에 멀티스레드 환경에서 작업할 수 있게 해주는 GCD 매카니즘을 시장에 내놓았다. iOS라는 기존 컴퓨터와 다른 특별한 환경에서 요구되는 빠른 반응성을 위해 무거운 과업을 Main Thread Queue가 아닌 Background thread Queue에서 실행해야만 한다. 이때 NSOperation 기술보다는 블록을 사용한 GCD가 더 가독성 높은 코드를 만들어 낼 수있기 때문에 많은 개발자의 사랑을 받고 있는 것 같다.

해당 블로그 포스트는 기본적인 멀티 스레드에 대한 개념과 GCD 라이브러리들에 대한 내용을 담을 것이다. 또한 본 포스트는 self-study에 대한 복습 정도의 메모를 주목적으로 하고 있기 때문에 자세한 설명이 없다면 댓글를 통해 문의 바란다.

멀티 스레딩의 기본 개념

  • 순차적(Serial) VS 동시적(Concurrent)

한 과업(Task)이 여러 과업들 중에 한번에 한개씩 실행된다면 ‘순차적(Serially)’ 진행이라 하고, 동시에 여러 과업이 동시에 실행이 된다면 ‘동시적(concurrently)’ 진행이다.

  • 과업 (Task)

이 글에서 설명하는 과업이란 Micro하게 보자면 오브젝티스-C에서는 ‘Block’으로 표현할 수 있고, Swift로 말하자면 ‘Closure’가 될 것이다. 블럭은 하나의 과업을 구현하고자 하는 코드 묶음이라고 할 수 있겠다. 그런데 이 블럭이나 클러져는 메서드의 파라미터나 인자, 혹은 클래스 인스턴스 Property가 되어 이곳 저곳으로 모듈화하여 사용되어 질 수 있다.

  • 동기식(Synchronous) VS 비동기식(Asynchronous)

동기식과 비동기식은 기본적으로 언제 과업을 호출한 곳에서 다음 코드 진행에 관한 컨트롤 권한을 넘겨 줄 것인가에 대한 개념이다. 예를 동기식은 자신의 과업 (여기에서 클러져나 블럭으로 표현되어 질 수 있다.)이 다 끝나고 코드 진행을 시키기 위해 컨트롤 권한을 준다면 ‘동기식’이고, 자신의 과업이 혹은 블럭이 다 완료 되지 않은 상황 가운데, 다음 코드로 넘어가게 한다면 비동기식일 것이다.

  • 임계구역 (Critical Section)

이 개념은 반드시 동시에 실행되어서는 안될 코드 구역을 말하는 것이다. 멀티 스레드 프로그래밍에서 공유자원을 사용할 때 여러 스레드에서 동시에 접근할 수 있는 위험성 있는 지역을 보호하는 개념이다.

  • 경합 조건(Race Condition)

멀티 스레드 프로그래밍에서 두 명령어가 동시에 같은 기억 장소를 접근하고자 할때 그들 사이의 경쟁에 의해 수행 결과를 예측할 수 없게 되는 것인데, 이와 같은 현상은 바람직하지 않으므로 OS에서 이것을 해소해주어야 할 것이다. 출처

  • 교착상태 (Deadlock)

한개 이상의 스레드에서 서로가 필요한 작업이나 리소스를 기다리는 상태이다. 예를 들어 첫번째 스레드가 두번째 스레드가 끝나야 일을 진행 할 수 있고, 두번째 스레드는 첫번째 스레드가 끝나야 일을 진행 할 수 있는 경우이다. 이런 경우에는 이도저도 못하는 교착상태에 빠져 버린다.

  • 스레드 세이프 (Thread Safe)

스레드 세이프한 코드는 멀티 스레드나 동시 과업 환경에서 아무런 문제 없이 호출 되어지고 실행가능한 코드를 말한다. Swift 언어의 let으로 시작하는 변수가 ‘스레드 세이프’하다고 할 수 있겠다. 초기화 할때 정해진 값이 끝까지 변하지 않기 때문이다. 반대로 var로 시작하는 변수는 ‘스레드 세이프’하지 않다. 멀티 스레드 환경에서 언제든지 읽혀지고 쓰여질 수 있기 때문이다.

  • 문맥 교환 (Context Switch)

한 프로세스 안에서 하나 이상의 스레드에서 다른 스레드로 작업환경을 변경하고자 할 때 저장과 불러오기의 과정을 문맥 교환이라고 한다. 문맥 교환이 잦아지면 성능의 문제가 있다. 최소화하는 것도 성능개선에 중요한 부분이다.

  • 동시성(Concurrency) VS 병렬성(Parallelism)

동시성은 두 개 이상의  과업이 시작, 실행, 완료등이 같은 시간대에서 행하여지는 것을 뜻한다. 그런데 주의 할 것은 두 과업이 반드시 같은 순간에 실행되는 것을 의미하지 않는다. 예를 들어, 싱글 코어 CPU에서 시간을 쪼개어 동시에 작업 중인 과업을 왔다갔다 하면서 연산한다. 반면 병렬성은 말그대로 동시에 과업을 수행하는 것을 뜻한다. 멀티 코어 CPU에서 가능하다.

출처. raywenderlich.com

출처. raywenderlich.com

Queue

GCD에서는 스레드 풀은 크게 두 가지 타입이 있는데, 순차적 큐와 동시적 큐이다. GCD를 사용하게 되면 스레드를 직접 생성 혹은 호출하여 사용하지 않는다. 스레드 풀인 큐에서 넣어 놓고 과업을 실행하게 되는데, 자신의 상황에 맞게 사용하기 위해서는 어떤 것들이 있는지 알아야 할 것이다.

  1. 순차적 큐(Serial Queue): 한번에 하나의 과업이 순차적으로 실행되어지는 스레드 큐를 뜻한다.  과업이 실행되는 순서는 FIFO(First In First Out)이다. GCD에서 dispatch_get_main_queue가 대표적인 순차적 큐이다. UI 작업은 메인 큐에서만 해야한다. 하지만 사용자 정의 순차적 큐도 만들 수 있다. 큐를 생성할 때는dispatch_queue_create(“com.bartysways.Project”, DISPATCH_QUEUE_SERIAL)와 같이 ‘DISPATCH_QUEUE_SERIAL’ 파라미터를 지정해주면 된다.
  2. 동시적 큐(Concurrent Queue): 동시적 큐는 비동기적인 작업을 할 때 사용하기에 알맞은 큐이다. 과업의 시작 순서는 전적으로 GCD가 판단을 하게 되어지는데, 어떤 코어에서 어떤 것을 먼저 실행하고 완료되어질지는 GCD와 과업의 종류에 따라 달렸다. dispatch_queue_create(“com.bartysways.Project”, DISPATCH_QUEUE_CONCURRENT).
출처: raywenderlich.com

출처: raywenderlich.com

Quality of Service

GCD에서 제공하는 QoS 클래스가 있다. 이 클래스는 GCD가 동시적 큐에서 과업의 우선순위를 정할 때 고려되어지는 클래스로써 과업의 의도를 추상화 하여 표현하여 네이밍을 하였다. QoS를 사용하여 GCD를 사용할 때의 장점은 각 디바이스 아키텍쳐에 맞는 환경을 시스템이 고려하여 멀티 스레딩을 효율적으로 해준다는 것에 있다. 물론 일일히 NSThread를 사용하여 관리 하여 코딩할 수 있지만, 많은 코딩과 많은 복잡도를 애플이 제공해주는 메카니즘에게 맡기고 그 혜택을 누릴 수 있는 것이다.

  • QOS_CLASS_USER_INTERACTIVE: 해당 클래스는 UI 업데이트를 위한 과업을 실행할 때 명시하는 클래스이다. 이 클래스를 사용하게 되면 과업은 순차적으로(동기식) 항상 Main Queue에서 실행된다.
  • QOS_CLASS_USER_INITIATED: 해당 클래스는 UI 이벤트를 통해 초기화 작업을 해야 할 때를 위해서 제공된다. 관련된 과업은 비동기식으로 실행된다. 예를 들어 사진 필터를 제공하는 기능이 있다고 가정을 하자, 원하는 필터 효과를 적용하기 위해서 버튼을 탭하고 해당 이미지를 필터에 맞는 효과를 적용할 때 필요한 과업을 해당 클래스를 지정하여 실행하면 효과적이다.
  • QOS_CLASS_UTILITY: 파일 I/O 작업이나 네트워킹 작업 같이 긴 시간을 필요로 하는 작업에 적당한 클래스이다.
  • QOS_CLASS_BACKGROUND: 이 클래스는 시간에 구애 받지 않고 사용자가 신경쓰지 않아도 되는 작업들을 백그라운드에서 실행하고자 할때 사용 되어지도록 디자인된 클래스이다. 예를 들어 로그아웃 시 로컬에 저장되어 있는 파일을 지워야 한다든지 혹은 이지미 파일을 다운로드 받은 후 캐싱을 한다든지 와 같은 작업을 할 때 적당할 것이다.

 

Practical Example

dispatch_async

  1. override func viewDidLoad(){
  2.    let queue = dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.value), 0)
  3.    dispatch_async(queue){
  4.        println("First Block")
  5.    }
  6.    println("Second Block")
  7. }

위 코드의 실행 결과는 ‘Second Block’이 먼저 실행된 다음 ‘First Block’이 실행된다. 글로벌 큐에서 dispatch_async를 하게 되면, 클로져 안에 들어 있는 코드는 메인 큐가 아닌 글로벌 큐에 들어가서 자신의 순서가 되어 있을 때 실행되어진다. 여기에서 말하는 글로벌 큐는 메인 큐가 메인 스레드를 뜻한다면, 새로운 스레드를 뜻하는데 소위 백그라운드 스레드라고 해도 무관할 것이다. (참고로 글로벌 큐는 해당 디바이스의 CPU 코어 갯수에 따라 갯수가 정해진다.)

dispatch_sync

  1. override func viewDidLoad(){
  2.     super.viewDidLoad()
  3.  
  4.    let queue = dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.value), 0)
  5.    dispatch_sync(queue){
  6.        println("First Block")
  7.    }
  8.    println("Second Block")
  9. }

위 코드의 실행 결과는 ‘First Block’, ‘Second Block’ 순으로 실행이 된다. 물론 메인 큐가 아닌 글로벌 큐에서 작업이 실행 되지만, 해당 클로저의 코드가 다 끝나기 전까지 메인 스레드에게 통제권을 넘기지 않는다.

dispatch_after

  1. func executeDelay() {
  2.  
  3.     let delayInSeconds = 3.0
  4.     let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delayInSeconds * Double(NSEC_PER_SEC)))
  5.     let queue = dispatch_get_main_queue()
  6.  
  7.     dispatch_after(popTime, queue) {
  8.         println("Bang! Bang! Bang!")
  9.     }
  10.   }

위 코드는 3초 후에 클로져의 코드가 실행된다.

dispatch_once

  1. private var signalOnceToken = dispatch_once_t()
  2.  
  3.   override func viewDidLoad() {
  4.     super.viewDidLoad()
  5.  
  6.     dispatch_once(&signalOnceToken) {
  7.         println("Just one time!")
  8.     }
  9. }

dispatch_once는 프로세스가 떠 있는 동안 한번만 실행했으면 할 때 사용을 한다. 싱글톤 패턴을 구사 할 때 사용해도 무관하다.

Readers and Writers 문제 핸들링

dispatch_barrier_async

GCD의 barriers API를 ‘동시적큐’에서 사용하면 관련 과업은 타과업과 동시에 실행되지 않고 단독 독점을 하여 실행하게 된다. 이 API를 사용하여 읽기 작업이나 쓰기 작업을 한다면 두 개 이상의 Task에서 동시 접근을 막을 수 있게 된다.

  1. private let concurrentPhotoQueue = dispatch_queue_create(
  2.     "com.raywenderlich.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT)
  3.  
  4. func addPhoto(photo: Photo) {
  5.  
  6.   dispatch_barrier_async(concurrentPhotoQueue) {
  7.  
  8.     self._photos.append(photo)
  9.  
  10.     dispatch_async(GlobalMainQueue) {
  11.       self.postContentAddedNotification()
  12.     }
  13.   }
  14. }

위 예제에서 ‘_photos’ Array 개체에 새로운 값을 넣어 줄 때, dispatch_barrier_async 함수를 사용한다면 스레드 안전하게 실행할 수 있게 된다.

Screen Shot 2015-07-16 at 4.47.45 PM

위 그림을 살펴보자. 동시적큐에서 여러 과업이 병렬되어 실행되고 있는 모습을 볼 수 있다. 그런데 ‘Barrier Task’ 블럭을 보면 단독적으로 시간을 보내는 모습을 볼 수 있다.

Dispatch Apply && Dispatch Groups

Dispatch group은 그룹의 묶음으로 지정된 과업이 다 마쳤을 때 notify해주는 기능을 가지고 있다. 실예로 iOS 프로젝트에서 UITableView를 사용할 때 화면에 보이는 각 cell마다 thumbnail 이미지를 다운로드 과업들이 다 끝났을 때 알림을 받고 싶다면 해당 API를 써서 구현할 수 있다. 아래 예제를 보고 확인해보자.

  • dispatch_apply: for 반복문 처럼 동작한다.
  • dispatch_group_create:
  • dispatch_group_enter
  • dispatch_group_leave
  • dispatch_group_wait
  • dispatch_group_notify: 비동기식으로 그룹이 들어온 만큼 다 나가면 디스패치 된다.

  1. func downloadWithCompletion(completion: DownloadingCompletionClosure?) {
  2.  
  3.     var storedError: NSError?
  4.     var downloadGroup = dispatch_group_create()
  5.     let addresses = ["http://www.....", "http://www.....", "http://www....."]
  6.     let queue = dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)
  7.  
  8.     //#1 다운로드 반복 구문
  9.     dispatch_apply(addresses.count, queue) { (i: Int) -> Void in
  10.  
  11.       let address = addresses[i]
  12.       let url = NSURL(string: address)
  13.  
  14.       dispatch_group_enter(downloadGroup)
  15.  
  16.       let item = Download(url: url!) {
  17.         image, error in
  18.         if error != nil {
  19.           storedError = error
  20.         }
  21.         dispatch_group_leave(downloadGroup)
  22.       }
  23.       ItemManager.sharedManager.addItem(item)
  24.     }
  25.  
  26.     //#2 다운로드 완료시 실행되는 부분
  27.     dispatch_group_notify(downloadGroup, GlobalMainQueue){
  28.  
  29.       if let completion = completion {
  30.         completion(error: storedError)
  31.       }
  32.     }
  33.   }

Dispatch Block

iOS8에서 새로 추가된 Dispatch Block은 기존에 사용하던 클로져와 같은 개념이나 추가된 기능을 포함하고 있다. QoS와 같은 큐안에서 우선순위를 정할 수 있는 기능이 추가 되었으며, 무엇 보다도 취소(Cancelable) 가능해졌다. 하지만 기억해둬야 할 점은 취소라는 기능이 뜻하는 것은 대기중인 과업이 큐에서 빼주는 것을 의미한다. 그러므로 만약 과업이 자기 차례가 돌아와 벌써 실행이 시작되었으면 과업은 끝까지 실행을 하고 말것이다.

  • dispatch_block_create
  • dispatch_block_cancel

  1. //#1. dispatch_block을 사용해 dispatch_async 사용하기
  2. let queue = dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)
  3. let block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS){
  4.     println("Hello")
  5. }
  6. dispatch_async(queue, block)
  7. .
  8. .
  9. .
  10. //#2. 취소하기
  11. dispatch_block_cancel(block)

GCD Testing

GCD는 멀티스레드 프로그래밍을 보다 쉽고 효율적으로 관리 실행해주게끔 도와주는 것 이상으로 단위 테스트를 할 때도 유용하게 사용할 수 있다. 예를 들어 비동기식 서버통신을 테스트를 할 땐 GCD에서 재공하는 Semaphore나 XCTest 프레임 워크에서 제공하는 XCTestExpectation을 통해서 테스트할 수있다. 필자는 해당 기능이 소개되기 전 비동기식 서버통신에 관련된 단위 테스트를 짜기위해 여러가지 고민했던 기억이 난다. 그때 당시 내린 결론은 비동기식 단위 테스트는 불가능하니 서버 통신이 가지고 올 수있는 모든 단위 케이스들을 테스트했던 기억이 난다. 아무튼 비동기식 단위 테스트를 어떻게 할 수 있는지 살펴보자.

  • Semaphores: 프로세스간 메세지 전송을 하거나, 혹은 공유 자원을 통해서 특정 data를 공유하게 될 경우 발생하는 문제는, 공유된 자원에 여러개의 프로세스가 동시에 접근을 하면서 발생한다. 단지 한번에 하나의 프로세스만 접근 가능하도록 만들어 줘야 하고, 이때 세마포어를 쓴다. 출처 보통 스레드 차원에서는 뮤텍스, 프로세스 차원에서는 세마포어를 통해 멀티 스레드 환경에서 개발하게 된다.
  • expectation: XCTest 프래임워크에서 비동기식 코드를 테스트할 수 있겠금 제공해주는 방법이다.

Semaphores

세마포어는 한 공유자원에 접근할 수 있는 Thread의 수를 정의 해놓고, 접근을 제어하는 도구이다. 보통 ‘뮤텍스랑 세마포어’를 비교할 때는 화장실 예를 사용한다. 화장실를 사용하기 위해서는 화장실 열쇠가 있어야 한다. 뮤텍스는 하나의 열쇠를 가지고 화장실 전체를 사용할 수있는데, 만약 앞사람이 사용중이라면, 다른 사용자의 접근을 차단하고 사용이 끝나면 다른 대기자가 사용을 할수 있도록 허용하는 방식이라고 할 수있다. 반면 세마포어는 뮤텍스와 맞찬가지로 화장실을 사용하기 위해서는 열쇠가 필요한데, 각 비어있는 칸마다 열쇠를 나누어주고 화장실 입구 지키미가 접근을 관리하는 방식이다. 만약 남은 열쇠가 없다면 대기해야 한다.

자 그럼, GCD를 사용하여 Semaphore를 사용하는 방법을 알아보자.

  1. func downloadImageURLWithString(urlString: String) {
  2.     let url = NSURL(string: urlString)
  3.     let semaphore = dispatch_semaphore_create(0) // 1
  4.     let photo = DownloadPhoto(url: url!) {
  5.       image, error in
  6.       if let error = error {
  7.         XCTFail("\(urlString) failed. \(error.localizedDescription)")
  8.       }
  9.       dispatch_semaphore_signal(semaphore) // 2
  10.     }
  11.  
  12.     let timeout = dispatch_time(DISPATCH_TIME_NOW, DefaultTimeoutLengthInNanoSeconds)
  13.     if dispatch_semaphore_wait(semaphore, timeout) != 0 { // 3
  14.       XCTFail("\(urlString) timed out")
  15.     }
  16. }

차단을 원하는 자원에 대해서 서메포어를 생성하면 해당자원을 가르키는 세마포어 값이 할당된다. 이 값이 0이면, 다른 프로세스는 해당 자원에 접근할 수 없고, 0 보다 크면 해당 자원에 접근할 수 있는 개념이다. dispatch_semaphore_create(0)을 통해서 세마포어 개체를 초기화하고, 과업이 다 끝나면 dispatch_semaphore_signal(semaphore)를 통해서 다른 프로세스의 접근을 허용할 수 있겠금 신호를 보내준다. dispatch_semaphore_wait(semaphore, timeout)를 통해서 코드 진행을 정해진 시간만큼 블럭해준다.

expectation

XCTest에서 제공해주는 메카니즘으로 expectation 개체를 생성하고 작업이 다 끝나 해당 개체의 fulfill()이 불려지기 전까지 코드 진행을 멈춰서 기다리게 하는 방법이다. 예제 코드는 다음과 같다.

  1. func downloadImageURLWithString(urlString: String) {
  2.   let url = NSURL(string: urlString)
  3.   let downloadExpectation = expectationWithDescription("Image downloaded from \(urlString)") // 1
  4.   let photo = DownloadPhoto(url: url!) {
  5.     image, error in
  6.     if let error = error {
  7.       XCTFail("\(urlString) failed. \(error.localizedDescription)")
  8.     }
  9.     downloadExpectation.fulfill() // 2
  10.   }
  11.  
  12.   waitForExpectationsWithTimeout(10) { // 3
  13.     error in
  14.     if let error = error {
  15.       XCTFail(error.localizedDescription)
  16.     }
  17.   }
  18. }

문서 History

Update: 2015-09-15

Dispatch Group에 대한 간략한 설명 추가.

NSOperation VS Grand Central Dispatch

NSOperation vs. Grand Central Dispatch (GCD)

You may have heard of Grand Central Dispatch (GCD). In a nutshell, GCD consists of language features, runtime libraries, and system enhancements to provide systemic and comprehensive improvements to support concurrency on multi-core hardware in iOS and OS X. If you’d like to learn more about GCD, you can read our Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.

GCD라는 것을 들어봤을 것이다. 간략하게 설명하자면, GCD는 언어 기능과 런타임 라이브러리, 그리고 iOS와 OS X이 작동하는 멀티-코어 하드웨어 위에서 동시성을 지원해주는 시스템 강화 기능등으로 구성되어 있다. 만약 GCD에 대해 더 자세한 것을 알길 원한다면 블로그 ‘Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.’를 읽어보길 권유한다.

Before Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were different from GCD and used two completely different mechanisms. Starting with Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were built on top of GCD. As a very general rule, Apple recommends using highest-level abstraction, and then dropping down to lower-levels when measurements show they are needed.

OS X 10.6과 iOS 4 이전 버전에서는 NSOperation과 GCD의 메카니즘은 전혀 달랐다. 하지만 Mac OS X 10.6과 iOS 4 버전에서 부터 NSOperation과 NSOperationQueue는 GCD 위에서 설계되었다. 보통 애플은 개발자들에게 상위 레벨에 추상화 된 레이어를 먼저 사용하길 권유하고 있다. 그리고 나서 필요에 따라 낮은 레벨의 추상화된 기능들을 쓰길 권고한다. 

Here’s a quick comparison of the two that will help you decide when and where to use GCD or NSOperation and NSOperationQueue:

그럼 GCD를 써야 할지 혹은 NSOperation과 NSOperationQueue를 써야 할지 고민하는 당신을 돕기 위한 비교한 내용을 살펴보자.

  • GCD is a lightweight way to represent units of work that are going to be executed concurrently. You don’t schedule these units of work; the system takes care of scheduling for you. Adding dependency among blocks can be a headache. Canceling or suspending a block creates extra work for you as a developer! :]
  • GCD는 동시에 실행하려는 작업 단위를 대표 할 수있는 경량의 방법이다. 해당 작업 단위는 개발자가 직접 스케줄하지 않고 시스템이 스케줄 관리를 해준다. 블럭들 사이에서 의존성을 부여하는 것은 쉽지 않은 일이며, 작업 취소 혹은 일시정시 같은 일을 하기 위해서는 각 개발자가 개인별로 추가해야한다.
  • NSOperation and NSOperationQueue add a little extra overhead compared to GCD, but you can add dependency among various operations. You can re-use operations, cancel or suspend them. NSOperation is compatible with Key-Value Observation (KVO); for example, you can have an NSOperation start running by listening to NSNotificationCenter.
  • NSOperation과 NSOperationQueue는 GCD에 비해 추가적인 기능을 제공하며 여러 operation에 의존성을 부여할 수도 있다. 뿐만 아니라 재사용도 가능하며 취소 혹은 일시정지와 같은 기능도 가능하다. NSOperation은 KVO 기술을 완벽하게 사용할 수 있다. 그래서 NSOperation이 실행되기 시작하면 NSNotificationCenter를 통해 상태 변화에 대한 노티를 받을 수 있다. 

from http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues

[Objective-C 문법] GCD를 사용한 Concurrent Programming

개요

Concurrent Programming은 iOS 앱뿐만이 아니라 계산을 처리하는 CPU 코어가 하나 이상 사용되어지는 곳에서는 보다 효율적인 코딩을 위해서 사용되어 지는 기법이다. 사실 CPU는 한번에 한 가지의 계산 처리밖에 할 수 없는 존재이다. 하지만 워낙에 빠르기 때문에 시간을 쪼개어서 여러가지 일을 돌아가면서 실행하기 때문에 엔드 유저들에게 있어서 한번에 여러가지 일이 동시에 실행되어지는 것으로 착각하게 만든다. 하지만 하드웨어의 발전을 통해서 계산 처리 속도도 빠르게 업그레이드 했지만, 하드웨어 발전의 한계에 도달한 나머지 이제는 한 개의 코어 자체의 처리 속도를 올리려고 하기 보다는, 여러가지 처리르 동시에 할 수 있도록 여러 개의 코어를 한 CPU 칩에 탑재하는 방식의 최신 기술이 나오고 있는 실정이다.

하지만 이러한 혜택을 누리기 위해서는 하드웨어 뿐만이 아니라 소프트웨어 차원에 기법이 적용되었을 때 비로써 진정한 Concurrent 프로그래밍의 힘을 발휘할 수 있게 되는 것이다.

그렇다면 애플의 Objective-C를 사용한 프로젝트에서는 어떤 기법을 통해 Concurrent 프로그래밍을 할 수 있는지 알아보자. 먼저 애플은 다음 세가지 기법을 사용하여 Concurrent 프로그래밍을 할 수 있을 것이다.

  • GCD (Grand Central Dispatch)를 사용한 기법.
  • NSOperation이 포함되어 있는 Foundation 프래임워크를 사용한 기법.
  • NSThread 직접 쓰레드를 사용한 기법.

이번 포스팅에서는 GCD에만 집중하여 설명을 하게 될 것이다. 위의 기법이 어떻게 다른지 서로 비교하기 위해서는 먼저 하나하나 사용할 문법에 대해서 알아보는 것이 필요할 것이다.

1. 메인 큐에서 UI 관련 된 Task 실행하기

아이폰은 안드로이드 폰에 비해서 원활한 UI이 작동이 큰 장점 중 하나이다. 그것이 가능한 이유는 모든 UI 관련 처리는 main_queue에서만 처리를 하기 때문에 어떤 로직이 오래 동안 계산한다고 해서 UI 작동을 막지 못하기 때문이다. 그렇다면 어떻게해서 UI 관련 작업을 GCD를 통해 처리 할 수 있는지 아래 코드를 살펴보자.

  1. dispatch_queue_t mainQueue = dispatch_get_main_queue();
  2. dispatch_async(mainQueue, ^
  3. {
  4.     UIAlertView *alert = nil;
  5.     alert = [[UIAlertView alloc] initWithTitle:@"GCD TEST" 
  6.                                  message:@"HelloBarty" 
  7.                                  delegate:nil 
  8.                                  cancelButtonTitle:@"OK" 
  9.                                  otherButtonTitles:nil, nil];
  10.  
  11.     [alert show];
  12. });

 

2. UI 처리와 관련 없는 작업을 Synch하게 처리하기

큐에 들어 온 순서대로 처리하고 싶다면, 다음 코드를 참조하여 처리한다. 그런데 기억해야 할 것은 UI 작업이 처리될 main 큐에서는 순차적인 처리를 안해준다. (애플에서는 화면을 멈추게 하는 것을 막기위한 처신으로 보여진다.)

  1. void (^Print1To1000)(void) = ^
  2. {
  3.     NSUInteger counter = 0;
  4.     for(counter = 1; counter &lt;= 1000; counter++)
  5.     {
  6.         NSLog(@"counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  7.     }
  8. };
  9.  
  10. void (^Print1To100)(void) = ^
  11. {
  12.     NSUInteger counter = 0;
  13.     for(counter = 1; counter &lt;= 100; counter++)
  14.     {
  15.         NSLog(@"Counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  16.     }
  17. };
  18.  
  19. - (void)executeSynchrously
  20. {
  21.     dispatch_queue_t concurrentQue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  22.  
  23.     dispatch_sync(concurrentQue, Print1To1000);
  24.     dispatch_sync(concurrentQue, Print1To1000);
  25. }

 

3. UI 처리와 관련 없는 작업을 Asynch하게 처리하기

이번에는 순차적인 처리가 아닌 비동기적인 처리하는 법을 알아 보겠다. 크게 두가지 코드를 통해 알아 볼 것인다. 첫 번째 코드는 다음과 같다.

  1. void (^Print1To1000)(void) = ^
  2. {
  3.     NSUInteger counter = 0;
  4.     for(counter = 1; counter &lt;= 1000; counter++)
  5.     {
  6.         NSLog(@"counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  7.     }
  8. };
  9.  
  10. void (^Print1To100)(void) = ^
  11. {
  12.     NSUInteger counter = 0;
  13.     for(counter = 1; counter &lt;= 100; counter++)
  14.     {
  15.         NSLog(@"Counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  16.     }
  17. };
  18.  
  19. - (void)executeAsynchrously
  20. {
  21.     dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  22.  
  23.     dispatch_async(concurrentQueue, Print1To100);
  24.     dispatch_async(concurrentQueue, Print1To100);
  25. }

이번에는 네트워크를 통해 이미지 데이터를 다운로드 받아온 다음 ViewController에 집어 넣는 작업을 해보자.

  1. - (void)downloadImgAndShowAsynchronously
  2. {
  3.     dispatch_queue_t mainThread = dispatch_get_main_queue();
  4.     dispatch_queue_t concurrentQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  5.  
  6.     dispatch_async(concurrentQ, ^
  7.     {
  8.         __block UIImage *image = nil;
  9.  
  10.         //#. Download Task
  11.         dispatch_sync(concurrentQ, ^
  12.           {
  13.               NSLog(@"## 1");
  14.  
  15.               NSString *urlStr = @"http://yannickloriot.com/wp-content/uploads/2011/04/xcode.png";
  16.               NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
  17.               NSError *downloadError = nil;
  18.               NSData *imageData = [NSURLConnection
  19.                                    sendSynchronousRequest:req
  20.                                    returningResponse:nil
  21.                                    error:&downloadError];
  22.               if (downloadError == nil && imageData != nil)
  23.                   image = [UIImage imageWithData:imageData];
  24.               else if(downloadError != nil)
  25.                   NSLog(@"Error: %@", downloadError);
  26.               else
  27.                   NSLog(@"No data could get downloaded from URL..");
  28.           });
  29.  
  30.         //#. Show image Task
  31.         dispatch_sync(mainThread, ^
  32.           {
  33.               NSLog(@"## 2");
  34.  
  35.               if (image == nil)
  36.               {
  37.                   NSLog(@"Image isn't downloaded. Nothing to display.");
  38.                   return;
  39.               }
  40.  
  41.               UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
  42.               [imageView setImage:image];
  43.               [imageView setContentMode:UIViewContentModeScaleAspectFit];
  44.               [self.view addSubview:imageView];
  45.           });
  46.     });
  47. }

위 예제 코드를 보면 큰 Asynch 괄호에 두 개의 작업이 synch로 선언되어 있다. 이렇게 작업한 이유는 이미지 데이터가 다운로드가 끝난 다음에 ‘main 큐’에서 UI로 뿌려주기 위함이다. 하지만 본 블러그에서 잠시 언급했었지만, 원래 main 큐에서는 동기식 처리가 안되나, 이 작업은 비동기식 틀안에 있는 동기식 작업이기 때문에 처리가 허용된다. 만약 main 큐에다가 동기식 처리를 요청하게 되면은 Runtime 때 실행 조차 되지 않는다.

 

4. 작업을 지연하여 실행하기

GCD를 사용하여 작업에 타이머를 줄 수도 있다. 자동 Xcode 5의 자동완성 기능이 ‘dispatch_after’만 쳐도 코드를 완성해주는데 다음과 같다.

  1. double delayInSeconds = 5.0;
  2. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
  3. dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  4.  
  5. dispatch_after(popTime, concurrentQueue, ^(void)
  6. {
  7.     NSLog(@"Hello Barty!");
  8. });

 

5. 애플리케이션이 살아 있는 동안 한번만 실행하기

디자인 패턴에서는 ‘싱글톤 패턴’이 해당 기능의 역할을 한다. 앱이 살아 있는 동안 한번만 실행되게 해주는 기법인다. GCD를 통해서도 실현할 수 있다.

  1. void (^performThisOnlyOneTime)(void) = ^
  2. {
  3.     static NSUInteger numberOfEntries = 0;
  4.     numberOfEntries ++;
  5.  
  6.     NSLog(@"Executed %lu time(s)", (unsigned long)numberOfEntries);
  7. };
  8.  
  9. - (void)performOnce
  10. {
  11.     static dispatch_once_t onceToken;
  12.  
  13.     dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  14.     dispatch_once(&onceToken, ^
  15.     {
  16.         dispatch_async(concurrentQueue, performThisOnlyOneTime);
  17.     });
  18.  
  19.     return;
  20. }

그래서 실제로 싱글톤 패턴을 구현할 때 GCD를 사용하여 구현하는 경우도 많이 있다.

 

6. 그룹화 된 작업 처리하기

그룹화 된 작업을 처리하는 GCD가 있다.

  1. - (void)reloadTableView
  2. {
  3.     NSLog(@"reloadTableView");
  4. }
  5.  
  6. - (void)reloadScrollView
  7. {
  8.     NSLog(@"reloadScrollView");
  9. }
  10.  
  11. - (void)reloadImageView
  12. {
  13.     NSLog(@"reloadImageView");
  14. }
  15.  
  16. - (void)performGropingTasks
  17. {
  18.     dispatch_group_t group = dispatch_group_create();
  19.     dispatch_queue_t mainQueue = dispatch_get_main_queue();
  20.  
  21.     dispatch_group_async(group, mainQueue, ^
  22.     {
  23.         [self reloadTableView];
  24.     });
  25.  
  26.     dispatch_group_async(group, mainQueue, ^
  27.     {
  28.         [self reloadScrollView];
  29.     });
  30.  
  31.     dispatch_group_async(group, mainQueue, ^
  32.     {
  33.         [self reloadImageView];
  34.     });
  35.  
  36.     dispatch_group_notify(group, mainQueue, ^
  37.     {
  38.         UIAlertView *alertView = nil;
  39.         alertView = [[UIAlertView alloc] initWithTitle:@"Finished"
  40.                                                message:@"All tasks are finished"
  41.                                               delegate:nil
  42.                                      cancelButtonTitle:@"OK"
  43.                                      otherButtonTitles:nil, nil];
  44.  
  45.         [alertView show];
  46.     });
  47. }

 

7. 사용자 정의 큐 만들어 사용하기

사용자가 이름을 주고 순차적인 큐를 만들 수 있다. 보통 이름을 줄때는 ‘reversed domain 방식’으로 메겨진다 다음 코드르 살펴보자.

  1. - (void)performTasksOnSerialCustomeQueue
  2. {
  3.     dispatch_queue_t customQue = dispatch_queue_create("com.barty.personal.gcd", 0);
  4.  
  5.     dispatch_async(customQue, ^
  6.     {
  7.         for (int i = 0; i < 5; i++)
  8.         {
  9.             NSLog(@"#1. iteration: %d", i);
  10.         }
  11.     });
  12.  
  13.     dispatch_async(customQue, ^
  14.    {
  15.        for (int i = 0; i < 10; i++)
  16.        {
  17.            NSLog(@"#2. iteration: %d", i);
  18.        }
  19.    });
  20.  
  21.     dispatch_async(customQue, ^
  22.    {
  23.        for (int i = 0; i < 15; i++)
  24.        {
  25.            NSLog(@"#3. iteration: %d", i);
  26.        }
  27.    });
  28. }

 
복잡한 Thread 처리 없이 여러가지 기능을 제공해주는 GCD는 요즘 한참 유행처럼 많은 개발자들 사이에서 사용되어 지고 있다. 하지만 간편한 사용에 비해 ‘취소하기’와 같은 기능이 없어서 아쉬울 때가 있다. 무겁지 않은 가벼운 concurrent를 처리할 때에는 GCD만큼 간변하고 좋은 것이 없는 것 같다. 하지만 GCD가 채워주지 못하는 부분은 NSOperation이나 NSThread에서 처리를 해보자.