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

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

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

[Algorithm 관련] Reverse singly linked list

http://cslibrary.stanford.edu/109/list.gif

http://cslibrary.stanford.edu/109/list.gif

링크드 리스트는 Singly Linked list와 Doubly Linked list 타입을 갖는 데이터 구조이다. Singly Linked list는 자신의 값과 자신의 다음의 Node의 레퍼런스를 가지는 구조이며, Doubly Linked list는 자신의 값과 자신의 전의 Node의 레퍼런스와 자신의 다음에 이어지는 Node의 레퍼런스를 가지는 구조이다. 한번의 연속된 메모리를 할당하지 않는 특성을 가졌지만, Traverse를 하기에 꽤 비싼 비용이 드는 데이터 구조임에 틀림 없다.

Singly Linked list에서 어떻게 하면 reverse를 시킬 수 있을 까? 이것이 오늘의 알고리즘의 문제이다.

Time Complexity를 O(n)으로 풀수 있는 iterative한 방법이다. 먼저, 세개의 node를 저장할 변수를 만든다. prvNode, crrNode, nextNode. crrNode가 nil 아닐 때 까지 Loop를 돈다. 그러면서 치환을 통해 리스트를 반대로 돌릴 수 있다. 다음은 Objective-C 코드이다.

  1. - (LLNode *)reverseLinkedList:(LLNode *)header
  2. {
  3.     if (header == nil) {
  4.         return nil;
  5.     }
  6.  
  7.     LLNode *prvNode = nil;
  8.     LLNode *crrNode = header;
  9.     LLNode *nextNode;
  10.  
  11.     while (crrNode != nil)
  12.     {
  13.         nextNode = crrNode.next;
  14.         [crrNode setNext:prvNode];
  15.         prvNode = crrNode;
  16.         crrNode = nextNode;
  17.     }
  18.  
  19.     return prvNode;
  20. }

[Algorithm 관련] 순열 Permutation -Recursively-

신발 끈을 묶는 방법을 글로 쓰라고 하면 써볼 수 있겠는가? 저자는 자신이 없다. 글 재주가 없어서 그럴수도 있고 혹은 손에 익은 기술을 한번도 말로 표현해보려 하지 않아서 일지도 모른다. 순열, 즉 permutation 알고리즘 문제가 저자한테는 같은 느낌이다. 종이 위에 써보라고 하면 큰 어려움 없이 써 내려 갈수 있지만 막상 pseudo 코드나 과정을 설명하는 글을 써보라고 하면 자신이 없다.

먼저 permutation가 무엇인지 알아보자. 영어로 anagram이라고 표현하는 것도 보았다. 간단한 예를 들어 설명을 하자면 ‘ABC’의 순열은 다음과 같다.

ABC, ACB, BAC, BCA, CAB, CBA

알파벳 순서를 바꾸어서 만들어 낼 수 있는 String들을 말한다. 참고로 n개의 알파벳의 permutation 수는 n! 이다. 위 예는 알파벳 수가 3개이므로 3 * 2 * 1 = 6개의 조합이 나올 수 있다. Permutation을 구하기 위한 가장 쉬운 방법은 ‘각각의 알파벳 하나씩을 시작으로 하는 permutation’의 조합을 찾으면 된다. 무슨 말인지 모르겠다면 다음를 한번 살펴보자:

for(each possible starting letter) {list all permutation that start with that letter}

for 반복문 (각가의 처음에 시작 가능한 글자들) {해당 글자로 시작하는 모든 순열 리스트}

보통 재귀함수를 사용할 때는 for문과 같은 반복문을 사용하지 않는다. 왜냐하면 재귀적 호출 자체가 반복하는 것이기 때문이다. 하지만 permutation 문제에서는 예외라고 할 수있다. 시작 가능한 각각의 글자를 반복 문으로 돌면서 그 속안에서는 해당 글자로 시작하는 모든 순열을 재귀적 호출로 만들어 내는 것이다.

재귀적 함수의 기본으로는 기본 조건(Base condition)이라는 것이 있다. 이것은 무한 반복이 되지 안도록 탈출구를 만들어 놓은 것인데, 해당 알고리즘에서도 for 반목문을 사용하지만 동일하게 base condition이 존재해야한다.

  1. // Pre-condition: str is a valid C String, and k is non-negative
  2. //                            and less than or equal to the length of str.
  3. // Post-condition: All of the permutations of str with the first k
  4. //                             characters fixed in their original positions
  5. //                             are printed. Namely, if n is the length of str,
  6. //                             then (n-k)! permutations are printed.
  7. void RecursivePermute(char str[], int k);
  8.  
  9. for (j=k; j&lt;strlen(str); j++) {
  10.     ExchangeCharacters(str, k, j);
  11.     RecursivePermute(str, k+1);
  12.     ExchangeCharacters(str, j, k);
  13. }

위 Pseudo 코드를 살펴 보면 k라는 non-negative 인수가 있다. 이것은 첫 번째 글자의 인덱스를 저장한 parameter이다. 반복문을 돌면서 permutation을 구할 때는 자리를 바꾸면서 구할 것이고, k 값이 str의 길이와 같으면  출력을 하고 끝낼 것이다.

  1. void RecursivePermute(char str[], int k) {
  2.  
  3.      int j;
  4.  
  5.      // Base-case: All fixed, so print str.
  6.      if (k == strlen(str))
  7.          printf("%s\n", str);
  8.  
  9.      else {
  10.  
  11.          // Try each letter in spot j.
  12.          for (j=k; j<strlen(str); j++) {
  13.  
  14.              // Place next letter in spot k.
  15.              ExchangeCharacters(str, k, j);
  16.  
  17.              // Print all with spot k fixed.
  18.              RecursivePermute(str, k+1);
  19.  
  20.              // Put the old char back.
  21.              ExchangeCharacters(str, j, k);
  22.          }
  23.      }
  24. }

위 코드는 c 코드이다.

  1. @implementation ViewController
  2.  
  3. - (void)viewDidLoad {
  4.     [super viewDidLoad];
  5.  
  6.     [self printPermutation:@"abc"];
  7. }
  8.  
  9. - (void)didReceiveMemoryWarning {
  10.     [super didReceiveMemoryWarning];
  11.     // Dispose of any resources that can be recreated.
  12. }
  13.  
  14. #pragma mark – Private methods
  15.  
  16. - (void)printPermutation:(NSString *)str
  17. {
  18.     [self permutationHelper:str index:0];
  19. }
  20.  
  21. - (void)permutationHelper:(NSString *)str index:(NSInteger)k
  22. {
  23.     if (str.length == k) {
  24.         NSLog(@"%@", str);
  25.         return;
  26.     }
  27.  
  28.     NSInteger idx;
  29.  
  30.     for (idx = k; idx < str.length; idx++)
  31.     {
  32.         NSString *swappedStr = [self swapString:str onIndex:idx withNewIndex:k];
  33.         [self permutationHelper:swappedStr index:k+1];
  34.     }
  35. }
  36.  
  37. - (NSString *)swapString:(NSString *)str onIndex:(NSInteger)idx withNewIndex:(NSInteger)k
  38. {
  39.     unichar charForIdx = [str characterAtIndex:idx];
  40.     unichar charForK = [str characterAtIndex:k];
  41.  
  42.     NSString *strForK = [NSString stringWithCharacters:&charForK length:1];
  43.     NSString *tmpStr = [str stringByReplacingCharactersInRange:NSMakeRange(idx, 1)
  44.                                                     withString:strForK];
  45.  
  46.     NSString *strForIdx = [NSString stringWithCharacters:&charForIdx length:1];
  47.     return [[tmpStr stringByReplacingCharactersInRange:NSMakeRange(k, 1)
  48.                                  withString:strForIdx] copy];
  49. }
  50.  
  51. @end

Objective-C 코드이다.

 

[Obj-C] 개체 복사하기 – NSCopying -

Objective-C에서 클래스 인스턴스를 복사할 때 구현 되어 있어야 하는 Protocol은 NSCopying이다. 해당 Protocol은 하나의 인터페이스를 제공한다.

  1. - (id)copyWithZone:(NSZone *)zone

여기에서 Zone은 메모리에 있는 데이터의 Segment들이 있는 구역을 뜻한다. 옛날 그 어느날에는 memory segment를 특정 영역에 생성하여 관래했다고 한다. 하지만 꽤 오래전 부터 사용하지 않는 기술이다. 지금은 모든 앱은 하나의 Zone을 가지고 있다. 그러므로 인수 값인 zone에 대해서 크게 신경 쓰지 말고 기본적으로 무엇인지에 대해서만 인지하고 있으면 될 것이다.

보통 개발을 하다가 복사를 하고 싶으면 NSObject에 있는 copy 메서드를 사용하거나 mutableCopy를 사용하여 mutable 개체를 복사하고 있을 것이다. 사용자가 정의한 개체 복사를 위해는 NSObject의 copy나 mutableCopy 메서드를 override해서 사용하면 안된다. Copy 메서드가 호출하는 copyWithZone: 메서드를 override해서 사용해야 한다.

  1. #import <Foundation/Foundation.h>
  2.  
  3. @interface EOCPerson : NSObject <NSCopying>
  4.  
  5. @property (nonatomic, copy, readonly) NSString *firstName;
  6. @property (nonatomic, copy, readonly) NSString *lastName;
  7.  
  8. - (id)initWithFirstName:(NSString*)firstName
  9.             andLastName:(NSString*)lastName;
  10.  
  11. - (void)addFriend:(EOCPerson*)person;
  12. - (void)removeFriend:(EOCPerson*)person;
  13.  
  14. @end
  15.  
  16. @implementation EOCPerson {
  17.     NSMutableSet *_friends;
  18. }
  19.  
  20. - (id)initWithFirstName:(NSString*)firstName
  21.             andLastName:(NSString*)lastName {
  22.     if ((self = [super init])) {
  23.         _firstName = [firstName copy];
  24.         _lastName = [lastName copy];
  25.         _friends = [NSMutableSet new];
  26.     }
  27.     return self;
  28. }
  29.  
  30. - (void)addFriend:(EOCPerson*)person {
  31.     [_friends addObject:person];
  32. }
  33.  
  34. - (void)removeFriend:(EOCPerson*)person {
  35.     [_friends removeObject:person];
  36. }
  37.  
  38. - (id)copyWithZone:(NSZone*)zone {
  39.     EOCPerson *copy = [[[self class] allocWithZone:zone]
  40.                        initWithFirstName:_firstName
  41.                              andLastName:_lastName];
  42.     copy->_friends = [_friends mutableCopy];
  43.     return copy;
  44. }
  45.  
  46. @end

위 코드에서 copyWithZone: 메서드의 copy->_friends를 주목하자. _friends는 internal 인스턴스이기 때문에 -> 문법을 사용했음을 기억하자.

복사 개념에는 ‘Shallow Copy’(얕은 복사)와 ‘Deep Copy’(깊은 복사)가 있다. 얕은 복사란 인스턴스 프로퍼티로 Collection 타입의 개체가 있을 때 컨테이너는 복사를 하되 속의 있는 내용물은 기존 컨테이너가 가지고 있던 레퍼런스를 포인트하고 있는 개념이다. 깊은 복사는 말 그대로 클론을 하나 만들어서 새로운 메모리 영역에 올리는 것을 뜻한다.  NSCopying는 기본적으로 얕은 복사를 한다. 그러므로 깊은 복사를 하기 위해서는 추가적인 조취가 필요하다.

Effective Objective-C 2.0 Matt Galloway

Effective Objective-C 2.0 Matt Galloway

위 사진을 보면 얕은 복사는 컨테이너는 복사가 되었지만 내용물은 원본 개체의 내용물을 가르키고 있는 것을 볼 수있다. 반면 깊은 복사는 클론을 만들어 낸 것을 볼수 있다.

  1. - (id)deepCopy {
  2.     EOCPerson *copy = [[[self class] alloc]
  3.                        initWithFirstName:_firstName
  4.                              andLastName:_lastName];
  5.     copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
  6.                                              copyItems:YES];
  7.     return copy;
  8. }

NSMutableSet의 initWithSet:copyItems: 생성자를 통해서 깊은 복사를 할 수 있다.

[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에 대한 간략한 설명 추가.

[++, --] 소소한 프로그래밍 이야기 – 1

프로그래밍을 하면서 정말 기본적인 개념인데 가끔 햇갈리는 것들이 있을 것이다. 크게 어려운 개념도 아닌데 원낙 가끔 사용하다 보면 생기는 현상인 것을 때로는 머리 나쁜 자신을 탓할 때가 있다. ^^

이 글은 다름이 아닌 ‘증강 연산자와 감소 연산자 (Increment and Decrement Operators)’에 대해서 간단한 노트이다. 보통 연산자 ‘++’와 ‘–’를 가장 많이 사용하는 경우는 아마도 for 문이 아닐까 싶다. 항상 별 큰 생각 없이 ‘i++’를 찍어왔었던 내가 문득 학부때 배운 기억이 나는데 prefix으로 붙이는 것과 postfix로 붙이는 것과 무슨 차이가 있는지 생각이 가물가물해서 찾아보았다.

i++i = i + 1 혹은 i += 1와 동일하게 i의 값을 1씩 올려준다.  i의 값만 하나씩 증강 시키거나 감소 시기키 위해 ‘–’를 사용할 때는 피연산자 앞에다가 붙이든, 뒤에다가 붙이든 상관이 없다. 하지만! 이 연산자는 값을 return 해주기 때문에 다른 변수에 할당하게 될 때는 이야기가 달라진다.

  • 만약 연산자를 변수 앞에다가 붙이면, 변수에 대입해주기 전에 값이 올라 가므로 기존 값에서 1이 더해진 값이 대입된다.
  • 하지만 만약 연산자를 변수 뒤에다 붙이면 변수에 값이 올라가기 전 먼저 대입이 이루워지므로 기존의 값이 할당이 된다.

코드를 살펴 보자.

Screen Shot 2015-04-21 at 5.38.57 PM

 

(귀찮아서 스크린 캡처를 했으니 아마 복사는 안될 것이다. 물론 간단한 코드를 복사할 사람은 없을 것을 예상 되지만.)

위 예제와 같이 prefix으로 연산자를 사용 하는 것과 postfix로 연산자를 사용하는 것과 다른 결과가 나오게 된다. 뭐 나만 기억을 못하는 정말 작고 사소한 것일지 모르지만 동감하는 분들이 분명 어딘가 있을 것으로 기대한다.

Objective-C는 무슨 Endian인가?!

시작하며..

개인적으로 Bit Manipulation에 대해 공부를 하다가 궁금한 부분이여서 얇은 구글링을 통해 Endian에 대해 알아보게 되었는데, 스스로는 잊어 버리지 않기위해 남김이며, 더 나아가 혹시 궁금해서 검색하는 분들에게도 도움이 되지 않을까 하는 약간의 기대감을 갖고 글을 적어 본다.

Endian이란 무엇인가?

현재 컴퓨터 설계 측면에서 보면 Big-Endian을 사용하는 컴퓨터가 있고, Little-Endian을 사용하는 컴퓨터가 있다. Unix와 Linux 프로세서에서는 Big-Endian을 사용하고 있고, Inter x32, x64 계열에서 Little-Endian을 사용하고 있다고 한다. 그렇다면 여기에서 말하는 Endian은 과연 무엇인가?

Endian은 메모리에 byte를 읽고 쓰는 순서에 대한 방식을 말한다. 32 bit 컴퓨터는 4 byte 단위로 메모리에 데이터를 읽고 쓰는 동작을 하는데, 이때 높은 byte 부터 쓰느냐 혹은 낮은 것 부터 쓰느냐 여부는 자신의 컴퓨터 CPU 설계 방식에 따라 달라지게 된다. 예를 들면, 빅 엔디안 컴퓨터에서는 16진수 “4F52″를 저장공간에 “4F52″라고 저장할 것이다 (만약 4F가 1000번지에 저장되었다면, 52는 1001번지에 저장될 것이다). 반면에, 리틀 엔디안 시스템에서 이것은 “524F”와 같이 저장될 것이다.(출처)

왜 중요한가?

그렇다면 왜 이렇게 byte를 쓰는 순서가 중요한 것일까? 그 이유는 서로 다른 컴퓨터가 채택한 Endian이 다르기 때문이다. 만약 혼자 프로그램을 짜고 자신의 컴터에서만 사용한다면 큰 문제가 없겠지만, 네트워크 통신을 해서 보낸 데이터의 Endian 방식과 받는 컴퓨터의 Endian 방식이 상이 하다면 뜻하지 않은 결과가 나오기 때문이다. 그렇기 때문에 사용하게 될 프로토콜에서 정의하는 Endian이 무엇인지 확인한 후 프로그래밍을 해야 하며, 때에 따라 Little-Endian을 Big-Endian으로 변경하는 코드를 넣어야 할 것이다.

증명 코드

그렇다면 iOS 개발자로써 궁금해지는 것이 있다. Objective-C은 과연 어떤 Endian을 기본적으로 사용하고 있는지 궁금할 것이다. 답을 먼저 말한다면 당연히 ‘Little-Endian’이다. 개인적인 생각은 Intel CPU를 사용하고 있으니 당연히 그럴 것이라고 생각을 했지만, 이를 증명하기 위한 코드를 간단히 짜보면 안다.

  1. #include
  2.  
  3. int main() {
  4.     long x = 0×44434241;//"DCBA"
  5.     char *y = (char *) &x;
  6.  
  7.     if(strncmp(y,"ABCD",4)){
  8.         printf("Big Endian\n");
  9.     }else{
  10.         printf("little Endian\n");
  11.     }
  12. }

StackOver Flow에서 발취함.
코드는 strncmp 함수를 사용하여 “DCBA” 문자열과 “ABCD”문자열을 비교하는 간단한 실험이다. 만약 Big-Endian로 저장이 된 것이라면 x 변수가 저장 될때 0×44, 0×43, 0×42, 0×41 순서되로 저장 될 것이다. (0×41는 “A”이다.) 하지만 Little-Endian으로 저장이 된다면 이 순서가 반대로 저장이 될 것이다. 코드를 돌려보면 알겠지만 console창에는 ‘little Endian’이 찍힌다.

마치며…

실 프로그래밍 생활에서 기본이 되기에 살포시(?!) 무시하고 지나갈 수 있는 내용이지만 remind하는 차원에서 읽었으면 좋겠다. 그래도 이해가 안간다면 링크에 자세한 설명이 나와 있으니 한번 읽어보면 쉽게 이해가 갈것이라 생각되어 진다.

[iOS] 로컬라이즈 텍스트 하기 (Localized String)

앱을 만들 때 이제는 국제화를 고려하지 않을 수 없는 시대가 되었다. 앱에서 사용되는 텍스트를 국제화하기 위해 애플에서는 Localization과 Internationalization을 제공하는데 이 블로그를 통해 Localization하는 방법을 간단히 알아본다.

 

A. 세팅하기.
1. 새 파일 추가한다. iOS->Resource->String File. 파일명은 ‘Localizable.strings’ 설정.
2. 추가하고 싶은 언어를 Project->Info->Localization에서 선택한다. (사진참조)
Screen Shot 2015-01-26 at 10.58.28 AM

3. Localizable.strings 파일을 선택하고, File Inspector 창을 열어서 Localize 버튼을 클릭한다. 그리고 나서 Drop down UI에서 ‘English’를 선택한다. (사진참조)

Screen Shot 2015-01-26 at 11.02.36 AM

4. 추가를 하면 오른쪽에 위치한 File Inspector의 Localization 항목에 English가 선택되어 있을 것이다. 새로 추가한 언어도 클릭하여 추가한다. (사진참조)

Screen Shot 2015-01-26 at 11.06.38 AM

 

5. 왼쪽 새로 추가된 Localizable.strings(Spanish)를 클릭하여 Key/Value형식으로 파일을 작성하면 세팅은 끝이다.

Screen Shot 2015-01-26 at 11.08.04 AM

 

B. 사용하기.
1. 파일 안에 형식은 “KEY” = “CONTENT”; 형식으로 한다.
2. 코드안에서 사용할 때는 ‘NSLocalizedString(@”"), nil)’ 메크로를 호출하여 사용한다.

[iOS] Keychain 기본 개념과 Wrapper Class 제공

Keychain은 무엇인가?

Keychain은 Mac OS X, iOS의 다양한 응용 프로그램에서 사용되는 비밀번호를 저장하는 암호화 되어 있는 저장소이다. 그런데 이것이 왜 필요한가 궁금해 할지 모른다. 예를 들어보자면, 맥북 사용자 혹은 iOS 기기 사용자는 여러가지 앱들을 사용한다. 메세지 앱, 소셜 앱, 유틸 앱, 금융 앱 등등. 각 앱마다 혹은 각 서비스를 사용하기 위해서는 ‘사용자 인증’ 과정이 필요하다. 본인임을 인증하는 과정은 현재 2015년 1월까지도 대부분이 ID & Password 방식으로 인증하는 방식을 사용하고 있다. 그렇기 때문에 각각의 비밀번호를 외우고 있어야하는 번거러움이 생긴다. 모든 서비스에 같은 비밀 번호를 설정하든지, 혹은 각 서비스 별로 사용하는 비밀번호를 안전한 곳에다가 적어두고 사용할 때마다 꺼내 보든지 해야하는데, 이러한 번거러움을 해결하고자 만들어진 것이 바로 Keychain이다. Macbook 사용자들은 Keychain에 저장되어 있는 비밀번호를 사용하기 위해서 컴퓨터 root 권한의 비밀번호를 치기만 하면 각각 앱, 혹은 서비스에서 Keychain에 저장한 오래되어 기억나지 않는 비밀번호를 사용할 수 있다. iOS 같은 경우에 Keychain은 각 앱에서 저장한 정보를  가지고 올 수 있는데, Provisioning profile 별로 사용경로가 구분이 된다. 쉽게 말하자면 같은 앱이라 할찌라도 만약 개발 단계에서 Profile이 바뀐다면 그전에 Keychain에 저장해 둔 정보를 빼올 수 없을 것이다.

 

언제 Keychain을 사용할 것인가?

앱 개발자 입장에서 생각해보자. 기기에 데이터를 저장을 할 수 있는 여러가지 방법이 존재한다. 각자의 경험에 따라 떠오르는 것이 다르겠지만,  기본적으로는 데이터베이스 혹은 Core Data, NSDefaultUser, Keychain, .plist 파일 등등. 저장할 방법도 다양하며 각각마다 만들어진 목적이 다르다. 그럼 Keychain은 언제 사용할 것인가? Keychain의 가장 큰 장점은 ‘보안’이다. 그렇기 때문에 사용자의 소중한 정보를 저장하고자 한다면 Keychain을 사용하는 것이 옳다. (분명 다른 매체를 통해서 데이터 저장이 가능하다. 분명히 해둘 것은 불가능한 것이 아니라 비밀번호, 혹은 각종 서비스의 API를 사용할 때 사용되는  secret Key, 혹은 Access Token 같은 보안이 중요한 데이터는 암호화를 하여 저장 관리하는 Keychain에 다가 저장하는 것이 옳은 방법이라는 것이다.)

 

어떻게 Keychain 서비스를 사용하는가?

애플에서는 Keychain 서비스 사용방법을 설명하기 위해 샘플소스를 공개하고 있다. Stack Overflow에 관련 질문에 대한 도 있으니 한번 살펴보기 바란다. 그리고 개인 Github에 Swift용과 Objective-C용으로 올려놓았으니 한번 사용해보기 바란다. 헤더 파일은 아래와 같이 간단하다. 저장하고자 하는 정보가 있다면 key와 함께 NSData 형식으로 넘기면 된다.

  1. @interface KeychainHelper : NSObject
  2.  
  3. + (BOOL)saveWithKey:(NSString *)key andData:(NSData *)data;
  4. + (NSData *)loadWithKey:(NSString *)key;
  5. + (BOOL)deleteWithKey:(NSString *)key;
  6. + (BOOL)clear;
  7.  
  8. @end

[C 언어] 포인터 기본 개념

iOS 개발자들은 여러가지 언어를 사용하여 개발을 할 수 있다. 주류는 C 언어 개열의 Objective-C일 것이며, 새로나온 Swift를 사용해서 개발을 참여 할 것이다. 그런데 학부때 배운 C 언어의 가장 헷갈리는 개념을 뽑으라고 한다면 ‘포인터’일 것인데, 알고리즘 책을 들고 공부하다 보면 대부분의 예제가 C 코드로 되어 있어 한번 더 remind할 겸해서 리서치 해본 노트를 올려본다.

pointer-to-pointer

 

 

Definition

  1. Pointer 변수: 값이 저장되어 있는 변수의 주소를 담고 있는 변수.
  2. Dereferencing a Pointer: 가리켜고 있는 주소의 해당하는 실제 값을 뜻함.
  3. * : 해당 주소에 있는 값 연산자 (Value at address operation)
  4. & : 주소 연산자 (Address of operator)
  5. -> : 구조체 값의 데이터 접근하기

When to use?

  1. * : 포인터 변수를 선언 할 때 / Dereferencing 할 때 사용됨.
  2. & : 변수의 저장 주소를 참조 할 때 사용됨.
  3. -> : (예. ptr -> name(*ptr).name 과 동일함)

이렇게 생각 해보자.

포인터 변수 == 편지 봉투

& 포인터 변수 == 봉투에 적힌 주소

*포인터 변수 == 주소가 가리키는 실제 건물

마치며…

블로그 검색하다가 찾은 내용을 간략하게 노트에 적은 것을 잊어버리지 않기 위해서 정리해보았다. 더 자세한 내용을 코드와 함께 보기 원한다면 아래 링크를 참조하자.

http://denniskubes.com/2012/08/16/the-5-minute-guide-to-c-pointers/