Programming/Mac & iOS

[iOS] 멀티코어 개발자를 위한 애플의 선물 GCD – Grand Central Dispatch

MB Kyle KWON 2014. 2. 1. 12:24

CPU 벤더들의 클럭 경쟁에서 멀티코어 형태의 경쟁으로 패러다임이 바뀐 지금. 서버 뿐 아니라 PC 그리고 모바일 단말에 사용되는 OS는 프로그래머들에게 멀티 코어 프로세싱을 지원해 줘야하는 숙명을 가지게 되는데..


이에 애플은 스노우 레오파드를 발표할 때 두가지 큰 기술을 개발자들에게 선물했다.

하나는 GCD (Grand Central Dispatch), 나머지 하나는 OpenCL 이다.


      GCD는 쉽게 말해 멀티코어 프로세서를 위한 Thread 프로그래밍을 OS에서 자동 관리 / 분배 해 주는 Mac OS에 내재된 C Library이다.  이말은 즉 프로그래머에게 자신이 만든 Thread를 어떻게 멀티코어 프로세서에 분산 시킬 것인가에 대한 고민을 없애 주었다는 말이다.  이 이야기는 나중에 보다 심도 있게 설명하겠다.

 

      OpenCL은 재밌는 개념인데, 남아도는 GPU의 코어들을 일반 컴퓨팅 연산에 사용 할 수 있게 지원하겠다는 말이다. 게다가 남아도는 CPU 코어도 같이 ! (아직 GCD와 OpenCL은 유기적으로 연계 된 것 같지는 않다.  내 개인적 추측으로는 Lion 혹은 그 이후에 유기적으로 연계된 GCD와 OpenCL을 보게되지 않을까 생각된다.)


       사실 GPU는 요즘 200개가 넘는 코어를 가지고 오직 그래픽 프로세싱을 위해서만 사용되는 정말 강력한 프로세서 유닛이다. 하지만 일반 프로그래머들에게는 CPU의 SSE류의 인스트럭션셋을 부동소스점 연산을 위해 꾸역꾸역 쓰던것만 허용되었다. 하지만 이제는 애플이 이러한 한계를 풀려고 작정을 한 것 같다. 이제는 프로그래머들이 컴퓨팅 연산을 위해  규격화된 표준 명령어 셋으로 CPU와 GPU의 멀티 코어를 자유롭게 쓸 수 있게 되었다.

이 자료에서는 위의 두가지 멋진 기술중 GCD에 포커스를 맞춰 기술하겠다.


참고 :  만약 시간의 여유가 된다면 아래의 동영상 자료를 보기 바란다. GCD와 OpenCL의 놀라운 마술을 볼 수 있다.
http://www.youtube.com/watch?v=nhbA9jZyYO4

 

1. GCD 소개

      사실 OS에서 지원하는 멀티코어 프로세싱은 애플에게만 있는것은 아니다.

이미 오래전부터 Parallel Programming을 지원하는 OS는 서버쪽으로 시작하여 윈도우 .NET  Framework 4.0의 TPL(Task Parallel Library) 그리고 Java 역시 JDK 7에 Fork/Join Framework를 포함한 JSR166y 등을 통해 멀티코어 프로세싱 프로그래밍을 지원한다.  그리고 정말 오래된, 애플의 오래전 경쟁자였던 BeOS까지..

새로운 기술은 아니지만 지금 애플에서는 GCD를 강력하게 밀고 있다.

이는 다음과 같은 이유에서다.


  • 기존의 어플리케이션은 자체 스레드를 사용하여 개발되었기 때문에 멀티코어 프로세서에 최적화 되지 못하였다.
  • 개발자는 단 몇줄의 코드 추가로 애플 OS단에서 지원하는 멀티코어 프로세서에 최적화된 어플리케이션을 만들 수 있다.
  • 이렇게 어플리케이션 코드를 변경해 놓으면 앞으로 CPU에 코어가 몇개가 붙던 알아서 OS단에서 멀티코어 프로세싱을 지원해 준다.
  • 그러므로 지금부터 GCD를 배워라.

      이것이 지금 우리가 GCD를 공부해야 하는 이유이다. 실제로 2장에서 GCD의 사용 예제를 보게 될 텐데 정말 기존 코드에 몇줄의 GCD코드 삽입으로 멀티코어 프로세싱이 지원되는것을 확인 할 수 있다.

      사실 애플은 iOS4.0부터 GCD를 기본 프레임웍에 포함 시켰다. 그리고 그 이전에는 GCD의 Object-C wrapper class인 NSOperation 및 NSOperationQueue 라이브러리 셋을 iOS2.0 시절부터 지원하였다. 재밌는건 iOS4.0이 발표되던 시절 애플의 어떤 모바일 단말에도 듀얼 코어가 실리지 않았었다.

GCD의 주요 목적은 딱 두가지 이다.


  1. MAC OS에서 비치볼 혹은 iOS에서 스크린 프리징을 없애는 것 !
  2. OS단에서  하나 이상의 CPU 코어에 Thread를 고루 분산 시켜 주는 것 !

      그럼 애플 OS는 어떻게 이러한 Thread를 여러개의 코어에 고루 분산시켜 주는지 살펴 보자.

      OS에는 하나 이상의 어플리케이션이 구동되며  각 어플케이션은 여러개의 Thread를 하나의 CPU에 처리 요청하게 된다. 코어가 하나인 CPU는 꾸역꾸역 이러한 Thread Task List를 수행하겠지만 아무래도 워커 스래드의 양이 많아 지거나 여러개의 어플리케이션이 동시에 heavy thread들을 task list에 등록한다면 어플리케이션들의 속도는 점점 느려지게 될 것이다.  예전에는 이를 해결하기 위해 CPU 클럭을 높였지만 지금은 CPU 코어 수를 늘린다. 그럼 OS는 어떤 숙제를 가지게 될 까?

      하나의 코어에서 수행되던 Thread들을 여러개의  코어로 고루 분산 시켜서 빠르게 Thread task list를 비워 나가게 지원해야 한다. OS는 유휴 코어를 체크 하고 스케줄링하며 이러한 일들을 윈할히 처리해 나갈 것이다. 그런데 또 다른 문제가 있다.


      어플들이 서로 Thread를 처리하기 위해 CPU를 차지하고 사용하려 한다면 ?

지금까지의 pthread 기법에서는 이러한 문제를 해결하기 무척 힘들다. 그럼 어떻게 프로그래머는 CPU의 유휴 코어를 탐지하고 다른 어플들에게 피해를 주지 않으면서 멀티코어 프로세서를 지원하는 Thread 프로그래밍을 할 수 있을까?


CPU의 유휴 코어를 누가 알겠는가?

지금 어떤 어플케이션이 CPU 코어를 점유 하고 있는지 누가 알 수 있나 ? 바로 OS이다.



프로그래머는 그냥 OS에 Task 코드 블럭만 넘기면 OS가 알아서 어플들의 Task들에 우선권 조정을 하고 Thread를 스마트하게 만들어줘서 유휴 코어에 할당하는 것이다.

게다가 Thread 풀링을 사용하여 Thread자원을 재사용까지 하면서…



      이것이 GCD의 핵심 기술이다. 예전 NSOperationQueue에서 Queue를 사용한 것 처럼 프로그래머는 GCD Queue를 이용하여 Task를 Queue에 전달하면 그걸로 끝이다.

정말로 이걸로 끝이다. 나머지는 애플 OS가 다 알아서 해준다. 어플들간의 Task 조율 부터 Thread 만들기, 유휴  코어에 Thread 배정하기, Thread 풀링으로 재사용 하기 등등.

프로그래머는 지겨운 Thread 프로그래밍을 할 필요가 없어 졌다.

      나중에 GCD코드를 보겠지만 소스 코드에 Thread 관련 코드가 완전히 없어져 버렸다.

그럼 GCD 를 어떻게 사용하는지 살펴 보자.


2. GCD 사용법

      CGD를 사용하려면 우선  Block 코딩에 익숙해 져야 한다. 안드로이드 프로그래밍을 할 때 Block은 자연스러운 코딩 방법이었지만 iOS에서는 4.0부터 Block코딩을 지원하기 시작하였다. 사실 나도 왜 Block코딩을 여지껏 지원안했는지 정말 궁금했었다. 그런데 애플은 이미 자기네들은 내부에서 C Level의 Block 코딩을 하고 있었던 거였다. 젠장.

LLVM 근간의 CLang 컴파일러 공식 지원이 그 첫번째 이고, 이미 NSOperation은 iOS2.0 때부터 지원되었던게 그 두번째 증거일 것이다.

      사실 Object-C를 지원하는 애플이 Block 코딩을 지원하는게 그닥 탐탁치만은 안았겠지만 이제는 C base Library인 GCD를 표면적으로 지원해야하니 어쩔 수 없이 Object-C에도 블럭 기법을 추가하게 된 것이다. 도랑 치고 가재 잡는 형상이 되 버린거 같다.

그럼 블럭 코딩에 대해 먼저 살펴 보자.


Block Coding

      “ 블록은 포인터형 함수가 아닌 오브젝트형 함수다” 라는 뜻만 알면 블럭코딩에 대해 모두 안다고 할 수 있다. 포인터형 함수를 스래드에 매칭한다고 가정해보자. 우선 여러 스래드에서 해당 함수를 호출하면 스래드들은 해당 함수 코드 블럭이 존재하는 포인터를 참조하게 된다. 만약 해당 함수에서 전역변수를 사용한다면? 그럼 thread-safe를 위해 lock나 mutual exclusion 코딩이 추가 되어야 할 것이다. 만약 유연한 함수 코드를 지원하기 위해 아규먼트를 사용한다면?  함수 블럭 코드의 유연성을 지원하기 위해 아규먼트의 수량이나 복잡도가 증가하게되고 이는 특정 컨텍스트 관리를 위한 로드 증가 및 콜백 관련 코딩이 지원되어야 하는 고통이 수반될 것이다. 오브젝트형 함수인 블럭은 해당 코드 블럭들을 Thread에서 호출할 때 각각 메모리 상에 로드해 놓음으로서 위의 문제를 조금 간단하게 우회시켜 버렸다. 애플에서는 다음과 같이 블럭을 설명하고 있다.


  • Start out on stack – fast!
  • Blocks are objects!
  • Can be copied to heap!

또한 다음과 같은 경우에 블럭을 사용하라고 권장하고 있다.


  • Enumerations
  • Callback notifications
  • With GCD, moving work off the main thread
  • With GCD, mutual exclusion, concurrency

블럭의 시작은 ^ 으로 시작된다. 이는 마치 포인터의 *를 사용하는 것과 비슷하다.

간단한 블럭 함수 선언과 실행절에서의 사용법은 다음과 같다.


선언절  (void (^)(void))                         (void (^)(BOOL finished))

실행절  ^{ ……}                                      ^(BOOL finished) { …….. }

사용예제는 다음과 같다.

 

 

      위의 예제를 실은 이유는 코드블럭에 이름을 지어주는 방법에 대해 설명하고 싶어서이다. 그럼 GCD의 사용법에 대해 알아 보자. 아래의 예제는 일반적인 Objective-C 코딩이다.



1 2 3 4 5 6 7 
-(void) processImage:(UIImage *) Image { UIImage *processedImage = [ImageHelper doProcessing:Image]; [galleryView setImage:processedImage]   ; }


      간단하게 이미지를 전달받아 이미지 프로세싱을 한 후 이를 갤러리 뷰에 표시해주는 코드 블럭이다. 이 프로그램 코드의 문제는 무엇일까? 바로 이미지 프로세싱을 하는 동안 화면 프로세스는 죽을 수 있다는 것이다. 즉 시쳇말로 화면이 얼어 버릴 수 있다.

왜 그럴까?

      이유는 워커 스래드를 메인 스레드 상에서 처리하기 때문이다. 프로그램 코드에서 메인 스레드와 워커 스레드를 분리해야 할 것이다. 그럼 위의 예제를 Thread 프로그램으로 다시 구성해 보자.



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 
UIImage *Image = nil;   ....   [NSThread detachNewThreadSelector:@selector(processImage:) toTarget:self withObject:Image];   -(void) processImage:(UIImage *) Image { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; UIImage *processedImage = [ImageHelper doProcessing:Image]; [self performSelectorOnMainThread:@selector(showImage:) withObject:processedImage waitUntilDone:YES]; [pool release];   }   -(void) showImage:(UIImage *)Image { [galleryView setImage:processedImage]   ; }


      위의 예제는 워커 스레드 정의(이미지 프로세싱) 및 워커 스래드 함수 호출( Thread 호출) 그리고 메인스레드로 작업 넘기기(갤러리뷰에 이미지 보이기) 등의 코드로 구현하였다.

위와 같이 Thread프로그래밍을 하면 적어도 화면이 어는 현상은 없어진다. 하지만 여전히 문제는 있다 과연 여기서 멀티코어 프로세서를 어떻게 지원할 것인가 ? 막막하다. 그러려면 왠지 코드가 복잡하고 코드 양도 많아질거 같다. 하지만 아래의 GCD 적용 코드를 한번 살펴보자.



1 2 3 4 5 6 7 8 9 10 11 12 
- (void) processImage:(UIImage *) Image {   dispatch_async(dqueue, ^{ UIImage *processedImage = [ImageHelper doProcessing:Image]; dispatch_async(dispatch_get_main_queue(), ^{ [galleryView setImage:processedImage]   ; }); }); }


      단 두줄 추가하였다. 이렇게 함으로써 멀티코어 프로세서가 지원되는 Thread 프로그램을 만든 것이다. Thread보다 쉽지 않은가? 적어도 나는 NSThread나 NSOperation보다는 간편하였다. 물론 위의 코드는 단적인 예이다. 하지만 대부분의 GCD 코딩은 위의 예제만 습득하여도 충분히 사용 가능하리 만큼 강력 했다.


GCD Queue에 대하여

GCD의 시작과 끝은 GCD Queue로 시작해서 끝난다라고 얘기해도 과언이 아닌거 같다.

그만큼 GCD Queue가 중요한데 그 이유는 다음과 같다.


  • 프로그래머는 테스크를 정의하고 GCD Queue에 던지면 나머지 Thread 관리는 OS단에서 다 알아서 처리해 버린다.
  • GCD Queue는 블럭의 경량화된 리스트이다.
  • 모든 GCD Task는 GCD queue로 Enqueue 된다.
  • 이후 모든 Task는 GCD Queue에 의해 자동으로 Thread 생성되어 Dequeue 된다.

그럼 GCD  Queue에 대해 좀 더 세밀하게 알아보자.

좀 엉뚱하지만 Thread의 메모리상 크기는 얼마일까? 보통 512KB로 잡힌다. 그럼 1000 개의 Thread를 동시에 실행하려면 얼만큼의 리소스가 소요될까? 대략 0.5GB로 아마 아이폰은 예전에 뻗었을 것이다. 실제 내 경험상 아이폰에서 Thread관련 처리를 할 때 동시에 5개 이상 처리는 무리다.

그런데 CGD Queue의 크기는 얼마일까? 256 byte이다.

이것이 바로 블럭의 경량화된 리스트 이다.

그리고 GCD Queue는 Task 하나당 Thread 하나를 만들지 않는다.

Task들이 FIFO방식으로 GCD Queue에 쌓이면 우선 OS 단에서 유휴 코어를 산정하고 Thread Pool에서 재활용 가능한 Thread를 해당 코어만큼 Task로 매칭하여 실행해 버린다.

개발자는 그저 Task가 100개가 되던 1000개가 되던 상관없이 그냥 GCD Queue에 넣어 버리면 끝인것이다..

그럼 GCD Queue에는 어떤것이 있을 까?



  • Main Queue

dispatch_get_main_queue()로 메인 스래드/메인 루프에 테스크를 매칭시킨다.

  • Private Queue

dispatch_queue_create()로 순서대로 큐의 작업을 실행 시킬 때 사용된다.

  • Global Queue

dispatch_get_global_queue()로 Thread pool 방식으로 동작하며, 모든 작업은

이 큐로 보내진 다음 임의의 스레드에서 비동기 실행된다.


 

이상으로 GCD 에 대한 설명을 마치려고 한다.



출처 : http://dev.kthcorp.com/2011/05/19/grand-central-dispatch-gcd-apple-ios/