본문 바로가기

개발/App Developer

Grand Central Dispatch



이런 이런  XCODE3에 붙잡혀 이렇게 좋은 기능을 묻혀두다시피 했다. 

간략하게 정리해보겠다.

GCD 기술은 멀티프로세싱에 사용되는 기능이다. (멀티스레딩)
애플은 GCC를 대체할 물건으로 LLVM에 투자를 해왔다.
어느새 LLVM도 2.0까지 개발되었고  XCODE4는 GCC를 기본 프로젝트 설정으로 되어 있는
XCODE3와 달리 LLVM2.0을 기본으로 지원하고 있다. (속도차이가 많이 남)

LLVM덕분에 가능해진 기술이  Block과 GCD이다.

아무튼  iOS 4.0 으로 업데이트 하면서 멀티테스킹이 지원되었다. 
덕분에 앱을 종료하지 않고 다른 앱에 갔다가 올수도 있게 되었다. 
하지만 동시에 불편해진 부분도 있다. 

바로 메모리 문제이다. 
여러 앱이 동시에 실행되기 때문에 메모리는 여유는 없을 수 밖에 없다. 
그만큼 앱의 활동영역이 줄어들기 때문에 쾌적하지 못한 반응이 일어날 수 있다. 

하지만 애플은 이 문제에 대해 다른 안을 제시했다. 
GCD 기술을 제공함으로써 메모리 부족의 문제를 로직적으로 개선된 성능으로 
커버하도록 한 것이다. 

이 GCD기능을 사용할려면 우선 Block 코딩을 알아야 한다. 
기존의 C 를 하던 분은 아시겠지만  closure 라고 부르는 코딩 기법이다. 
Objective-C 에서는 Block 이라고 부른다. 

예문 )
Blocks

C: repeat(10,^{ putc(d);});
Ruby: z.each {|val| puts(val +d.to_s)}
Smalltalk : 10 timesRepeat:[pen turn:d; draw]
LISP closure : (repeat 10 (lambda (n) (putc d)))

C++0x lambda: template [=](){ putc(d);}

Block은 간단히 말하면 코드를 객체화 할 수 있는 것이다. 
블럭은 우선 " ^ " 로 시작한다. 
" ^ " 은 블럭의 시작임을 알리는 구분자인것이다. 

먼저 C 에서의 함수 구조를 보면 

int foo(int arg1, bool arg2){

....
return 0;
}

먼저 돌려주는 변수형이 있고 없는 경우  void : int
함수명이 있습니다. :  foo
그리고 ' ( '  , ' ) '  닫으면서 파라메터 들이 들어가게 된다.
그리고 마지막으로 스코프 ' {} ' 내부에 코드를 넣은 후 돌려줄 변수형이 있으면 return을 해준다.
이것이 C 의 기본이다.

블럭에서는 단순히 이 구조에 함수명만 없는 것이다.
물론 대신 앞에 구분자 ' ^ ' 가 들어간다.

^int(int arg1, bool arg2){
....
return 0;


그래서 가장 기본의 블럭 코딩의 형태는 

^<리턴형 변수>(<파라메터들>){<코드>}

즉 돌려줄 변수 타입, 파라메터, 본문코드로 구성된다. 
그리고 리턴 변수명과 파라메터는 없을 경우 무시할 수도 있다. 

파라메터가 없는 경우 
^int{....}

리턴이 없는 경우 
^(int arg1){...}

둘다 없는 경우 
^{...}

블럭 내부에 들어가는 코드는 Objective-C 로 사용하면 된다. 
블록은 응용기법이 너무 많아 알아서 공부를 해야 한다. 

블럭코딩이 들어간 예

UIView 에는 애니메이션 효과를 구현해주는 기능이 있다. 

예를 들어 뷰를 이동시킨 후 투명도를 조절해서 사라지게 하는 효과를 구현한다 해보ㅕㄴ

view.center = CGPointmake(x,y);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:2.0];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(viewHide)];
view.center = CGPointMake(x2,y2);
[UIView commitAnimations];

먼저 해당뷰를 이동시키고 
이동하는 애니메이션이 종료되면  viewHide라는 함수가 실행된다. 

그럼 그 함수에서 투명도 설정으로 사라지도록 한다. 

-(void)viewHide{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:2.0];
view.alpha=0.0
[UIView ommitAnimations];
}

이런 식으로 애니메이션을 선언하고 재생 시간을 정해준 후 설정하는 방법으로 은근히 코드가 길어진다. 

그리고 문제는 첫 애니메이션이 끝나면  viewHide 라는 함수를 호출함으로써 
다음 애니메이션을 호출하게 되는 점이다. 
함수라는 점과 조작하는 view가 계속 살아 있는지 봐줘야 하는 부분도 문제이다. 

View가 전역이 아니라 처음 애니메이션을 실행하는 곳에서 사용되는 객체변수라면
viewHide에서 사용하기 위해 전역으로 올리거나 beginAnimations 함수에 
인자로 객체를 같이 보내주어야 한다. 

이 방법이나 저ㅏㅇ법이나 번거롭기는 마찮가지다 

그래서 추가된 새로운 함수로 

+(void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations

가 있다. 

간단하게 duration 은 시간이고 animations에는 블럭코딩이 들어간다. 

하지만 위에서 했던 것은 이중 애니메이션이다. 

그럼 다른 함수로는 

+(void)animateawithDuration:(NSTimeInterval)duration animations:(void (^)(void)animations completion:(void (^)(BOOL finished))completion 

이 있다. 

completion 인자에 블럭 코딩을 하나 더 받는 걸로 애니메이션에 끝났을 때 호출되도록 한 것이다. 

코딩을 하다보면 줄이 너무 길어져서 줄넘겨 본적이 있을 것이다. 
인자가 너무 많아서 넘기기도 하고 배열 같은건 아예 한줄한줄 넘기는 것이 알아보기 쉽다. 

블럭코딩을 사용한 함수들은 자연스럽게 줄을 넘겨서 코딩 할 수 밖에 없다. 

앞서 했던 코딩을 블럭 코딩 방식으로 구현해 보겠다. 

view.center = CGPointMake(x,y);
[UIView animateWithDuration:2.0
                                                      animations:^{
                                                                             view.center = CGPointMake(x2, y2);
                                                     }
                                                     completion : ^(BOOL finished){
                                                                       [UIView animateWithDuration:2.0
                                                                                animations:^{
                                                                                                view.alpha = 0.0;
                                                                                }]
                                                      }];


2초 짜리 애니메이션을 animations 에 넣고 작동 시키고 애니메이션이 끝나면 
completion 인자에 다른 블럭 코딩을 넣어서 새로운 애니메이션을 실행한 것이다. 

블럭 코딩의 특징은 함수내에서 선언된 객체도 유지된다는 것이다. 

시간차 때문에 코드가 실행되기 전에 객체가 해제되지 않을까 걱정할 수도 있지만
그 코드 내부의 객체는 전부 retain 올려서 관리됩니다. 

블럭코딩을 받는 인자의 경우 API마다 받는 조건이 있습니다. 
블럭코딩은 함수와 비슷한 구조라서 리턴이 있고 인자가 있습니다. 

앞서 본 애니메이션 함수에 animations인자의 경우 
(void (^)(void))animations
라고 선언되어 있습니다. 

이 경우 첫 단어는 void 이므로 리턴할 것이 없고, 
(^) 이후의 가로의 내용이 (void)이므로 인자도 없습니다. 

그래서 animations인자에는 ^{ 으로 시작하면 되는 겁니다. 

그리고 애니메이션 종료후 실행되는 블럭인 completion은 
(void (^)(BOOL finished))completion 
라고 선언되어 있습니다. 

리턴은 void이므로 당연히 없습니다. 
근데 이번에는 BOOL finished 가 하나 있습니다. 

그러니 당연히  ^(BOOL finished){ 로 만들어서 사용해야 합니다. 
이 finished인자는 애니메이션이 재생하다가 도중에 멈추는 경우를 비교하기 위한 겁니다. 
재생 도중에 다른 곳에서 애니메이션 취소를 할 수도 있기 때문입니다. 
보통 YES 입니다. 

이제 GCD 로 들어가 보면 

GCD 는 C 언어로 되어 있는 스레드 관리 기술입니다. 


계층으로는 UIKit 보다 하위로 

Application 층과 UIKit층이 대부분 오브씨를 사용하고 libSYstem(예:SQLite) 등이 C로 되어 있습니다. 

GCD를 사용할려면 먼저 #include 선언을 해주어야 한다. 

#include <dispatch/dispatch.h>

기왕이면 전역에 선언해 주고 사용하는 것이 편하다. 

먼저 블럭 코딩을 다룬 것은 GCD 는 블럭 코딩을 이용했기 때문이다. 
* GCD 는 B lock-based API 이다. 

블럭코딩 기반의 GCD 는 기존의 스데드( NSThread, NSOperation)에 비해 좀 더 편히 
코딩을 할 수 있고 무엇보다 이미 해둔 코딩에 어느정도 쉽게 적용 가능하다는 
장점이 있다. 

큐 방식을 사용하는 NSOperation 은 GCD 와 비슷하다. 
어찌보면 NSOpertion 은 GCD 의 객체형이라 볼 수 있다. 

NSOpertationQueue 가 있듯이 GCD 에는 dispatch_queue_t 가 있다. 

비교해보면 

큐를 만드는 부분 

NSOperationQueue *queue = [[[NSOperationQueue alloc]init]autorelease];

dispatch_queue_t dQueue = dispatch_queue_create("com.apple.testQueue", NULL);

NSOperationQueue 의 경우 오토 릴리즈가 걸려 있어서 이후 일이 다 끝나면 알아서 해제 된다. 
하지만 C  언어의 GCD는 생성했으면 해제를 해주어야 한다. 

dispatch_release(dQueue);

물론 리테인 카운터 개념을 따르고 있기 때문에 dispatch_retain 이라는 리테인 거는 함수도 있다. 

GCD의 첫함수 이다. 

dispatch_queue_creage

 큐를 생성하는 함수이고 인자로는 char  배열의 label 과 attr 이 있다. 
label 의 경우 큐에 이름을 지어 주는 격으로 그다지 없어도 된다. (NULL 처리 해도 문제 없음)
attr 경우 큐의 속성으로 설정하는 인자로 추정되지만 현재 API 문서에는 안쓰고 있다고 한다. 그냥 NULL

dispatch_queue_t 는 변수 형으로 큐를 선언할 때 사용된다. 

이제 큐를 만들었으니 작업을 넣어 보자 

NSOperationQueue 의 경우 작업을 NSOperation 으로 생성해서 넣었지만 
GCD는 블럭을 작업 단위로 판단한다. 

dispatch_async(dQueue,^{
....
});

dispatch_async 는 큐에 블럭을 넣는 일을 하는 함수이다. 
당연히 인자는 큐와 블럭을 받는다. 

다른 함수로 dispatch_sync 도 있다. 
이 두 함수의 차이는 코드를 실행하는 동안 기다리는 유무이다. 

async의 경우 큐에 블럭을 넣는 일을 한 후 바로 다음 코드로 넘어가지만 
sync 는 큐에 넣고 그 일이 끝날때까지 기다린다. 

기본 기능은 여까지이고 이제 응용해보자 

멀티 스레딩 NSOperation 로 이미지를 다운 받아서 메인스레드에 영향을 안주고 이미지 뷰에 띄우는 
기능이 가능하다 
이미지뷰에 카테고리로 함수를 만들고 내부에서 NSOperation으로 작업할 수 있다. 

순차적으로 본다면 
우선 이미지뷰( UIImageView)를 준비하고 
데이터를 다운받고 
이미지를 로딩한 다음
이미지뷰에 넣는 것이다. 

이걸 코드로 구현해 보자 

일단 이미지 뷰는 화면에 이미 만들어져서 넣어져 있다고 가정하고 

UIImageView *imageView = ...

1. NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWIthString:@"주소"]];

2. UIImage *image = [UIImage imageWithData:data];

3. imageView.image = image;

1과 2는 다운받는 시간과 순간적 로딩하는 시간이 있기 때문에 NSOperation 에서는 
이부분을 스레드로 돌린 뒤 3번을 메인 스레드로 돌아와서 지정하도록 했다. 

근데 이미 코드가 상당하다 
아무리 간단해서 클래스를 만드록 지정하는 하수 등 해야할 부분이 은근히 많다. 

그런데 위 코드를 GCD에서는 어떻게 할까?

dispatch_async(dQueue,^{
          NSData *data = [NSData datawithContentsOfURL:[NSURL URLWithString:@"주소"]];
          UIImage *image = [uIImage imageWithData:data];

          dispatch_sync(dispatch_get_main_queue(),^{
               imageView.image=image;
          });
});

함수가 하나 더 나왔다. 

dispatch_get_main_queuq()

단어 뜻 그대로 메인 스레드용 큐를 돌려주는 함수이다. 

즉 위의 코드는 먼저 전체 블럭을 dQueue에 담는다. 
dQueue 에서 실행되는 블럭은 다른 멀티 스레드로 돌아간다. 

블럭이 스레드에서 실행되면서 
먼저 데이터를 다운 받는 코드를 실행한다. 
그 다음 이미지를 불러 온다 

그리고 안에 구현된 두번째 브럵을 이번에는 메인 큐(메인스레드)에 넣는다 
이렇게 함으로서 화면에 관련된 코드는 메인 스레드 상에서 돌렸다. 

NSOperation 으로 구현해쓸 때와 사실상 같은 구조이다. 
하지만 코드가 더 쉽고 짧다. 

NSOperation 이나 NSThread 를 사용하는 경우 NSAutoreleasePool  을 사용해야 하지만
블럭코딩의 경우 필요가 없다. 

NSOperationQueue 의 경우 최대 동시 작업 진행 설징이 있다. 
기본값은 무한대라 큐에 작업을 넣는데로 바로 스레드가 생성된다. 

물론 스레드가 너무 많아서는 안디ㅗ기 때문에 보통 5ㄱ 정도로 제한해 둔다. 

하지만 GCD의 큐의 경우 무조건 한번에 하나만 실행된다. 

큐에는 직접 만든 큐와 글로벌큐, 그리고 메인 큐가 있다. 

메인 큐는 메인스레드 상에서 도리게 되어 있고 
dispatch_get_main_queue()

직접 만든 큐는 하나의 스레드를 할당 받고 사용한다. 
dispatch_queue_create

글로벌 큐는 큐에 넣는대로 바로 스레드를 만들어서 붙여준다. 
dispatch_get_global_queue();

즉 NSOperationQueue 에서 본다면 동시 실행 가능 작업수를 무한대로 하는 것이 
글로벌큐와 같은 역할이라고 보면 되고 
직접만든 큐의 경우는 하나로 설정했다고 보면 된다. 

그렇다면 보통 큐는 한 블럭이 끝나야 다음 블럭을 진행할 수 있는데 
어떻게 해야 동시 진행 구현할 수 있을까?

바로 글로벌 큐를 사용하는 방법이다. 
하지만 글로벌큐는 넣는 즉시 스레드로 만들기 때문에 반대로 제한을 줄 수 없다. 

그렇다면 만든큐에서 제한을 걸어서 글로벌 큐에 필요한 수 만큼만 넣으면 된다. 

카운트를 사용해서 루프 돌리며 기다리게 하는 방법은 그다지 추천하지 않는다. 
기다리는 동안 스레드를 돌리기 때문이다. 

일단 간단한 GCD 예문을 보자 



변수 앞에 _block 선언은 블럭들 안에 공통으로 사용하기 위한 선언이다. 

이렇게 하고 돌려보면

[Session started at 2010-08-17 00:49:44 +0900.]

2010-08-17 00:49:46.449 ThreadSample[21942:207] end

2010-08-17 00:49:46.449 ThreadSample[21942:1903] GCD: 0

2010-08-17 00:49:48.451 ThreadSample[21942:1903] GCD: 0 END

2010-08-17 00:49:48.452 ThreadSample[21942:1903] GCD: 1

2010-08-17 00:49:50.453 ThreadSample[21942:1903] GCD: 1 END

2010-08-17 00:49:50.454 ThreadSample[21942:1903] GCD: 2

2010-08-17 00:49:52.454 ThreadSample[21942:1903] GCD: 2 END

2010-08-17 00:49:52.455 ThreadSample[21942:1903] GCD: 3

2010-08-17 00:49:54.456 ThreadSample[21942:1903] GCD: 3 END

2010-08-17 00:49:54.457 ThreadSample[21942:1903] GCD: 4

2010-08-17 00:49:56.457 ThreadSample[21942:1903] GCD: 4 END

라고 뜹니다.

순차적으로 실행되긴하지만 결국 하나씩 진행되었습니다.

하지만 필요한 건 동시 진행이다. 

그래서 등장하는 것이  semaphore( 시그널) 이다. 
먼저 시그널 또한 하나의 객체로 취급된다. 

dispatch_semaphore_t


큐를 만드는 법과 비슷하게 시그널을 만듭니다


dispatch_semaphore_t execSemaphore = dispatch_semaphore_create(5);


dispatch_semaphore_create함수는 시그널을 만들어 주는 함수입니다

인자로 갯수를 받습니다. 대충 5개로 하면 스레드를 5개까지만 한다고 보면됩니다.

이렇게 만들어진 시그널은 해제 할때 dispatch_release로 해제해주면됩니다.



이 시그널에 관련된 함수는 3개입니다.


생성용인 

dispatch_semaphore_create

신호를 날리는 

dispatch_semaphore_signal

그리고 신호를 받는 

dispatch_semaphore_wait


이 시그널의 작동원리는 이렇습니다



먼저 하나의 블럭이 실행되면

wait을 호출합니다. 


그러면 시그널은 내부 카운트를 1 올립니다.


그리고 내부 카운트가 먼저 생성할때 설정한 5보다 적으면 바로 돌아가고 많을 경우 스레드가 다음 코드로 넘어가지 못하게 Sleep을 걸어버립니다.


그리고 다른 블럭에서 작업이 끝날때 signal을 날려주면 내부 카운트가 하나 줄게됩니다.


내부 카운트가 줄었을때 설정한 5보다 적으면 재우던 스레드를 깨워서 진행 시켜주는 것이죠


먼저 기존의 블럭을 블럭을 실행할때 내부에서 글로벌큐에 새로운 블럭을 넣도록 이중구조로 변경합니다.


 

그리고 실행해보면 넣는 즉식 전부 글로벌 큐에 넣기 때문에 실행 순서를 알아보기 힘듭니다.

[Session started at 2010-08-17 01:23:02 +0900.]

2010-08-17 01:23:03.775 ThreadSample[22365:207] end

2010-08-17 01:23:03.776 ThreadSample[22365:1903] GCD: 0

2010-08-17 01:23:03.776 ThreadSample[22365:5d03] GCD: 1

2010-08-17 01:23:03.778 ThreadSample[22365:6203] GCD: 3

2010-08-17 01:23:03.777 ThreadSample[22365:6103] GCD: 2

2010-08-17 01:23:05.776 ThreadSample[22365:1903] GCD: 0 END

2010-08-17 01:23:05.777 ThreadSample[22365:5d03] GCD: 1 END

2010-08-17 01:23:05.779 ThreadSample[22365:6203] GCD: 3 END

2010-08-17 01:23:05.782 ThreadSample[22365:6103] GCD: 2 END

3초에 동시에 전부 실행되고 2초후인 5초에 전부 정지된것을 볼수있습니다.

이제 시그널을 사용해 보겠습니다.




코드가 길어보이는건 여러개를 돌려보기 위해서 그냥 코드를 복사한것뿐입니다.


돌려보면

[Session started at 2010-08-17 01:31:08 +0900.]

2010-08-17 01:31:09.172 ThreadSample[23337:207] end

2010-08-17 01:31:09.173 ThreadSample[23337:5e03] GCD: 0

2010-08-17 01:31:09.174 ThreadSample[23337:5f03] GCD: 1

2010-08-17 01:31:11.175 ThreadSample[23337:5e03] GCD: 0 END

2010-08-17 01:31:11.176 ThreadSample[23337:5e03] GCD: 2

2010-08-17 01:31:11.177 ThreadSample[23337:5f03] GCD: 1 END

2010-08-17 01:31:11.178 ThreadSample[23337:6503] GCD: 3

2010-08-17 01:31:13.176 ThreadSample[23337:5e03] GCD: 2 END

2010-08-17 01:31:13.177 ThreadSample[23337:5e03] GCD: 4

2010-08-17 01:31:13.179 ThreadSample[23337:6503] GCD: 3 END

2010-08-17 01:31:15.177 ThreadSample[23337:5e03] GCD: 4 END

이렇게 출력되었습니다.

시간기준으로 보면 0, 1이 동시에 진행하고 2, 3이 진행되었으며 4가 마지막으로 진행될걸 알수있습니다.


원하던 데로 두개씩 돌린겁니다.


그럼 작동 원리를 자세히 보겠습니다.



반복된 코드중 하나만 보면


먼저 블럭으로 dqueue에 넣었습니다.


그리고 이 만든큐(dqueue)는 순차적으로 하나씩 실행할수 있습니다.


이 반복된 블럭이 5개 정도 큐에 들어있습니다.



구분을 위해 각각의 블럭에 들어간 순서대로 0, 1, 2, 3, 4라합니다

실제로 로그로 찍히는 번호도 같습니다.


먼저 0번 블럭이 dqueue에서 실행됩니다.


그럼 먼저 시그날의 wait을 실행합니다. 이때 시그날을 내부 카운트를 하나 올립니다.


처음 실행된것이므로 1이됩니다.


그리고 처음 만들때 설정한 2보다 작으므로 그대로 스레드를 진행합니다.


다음 코드는 글로벌큐에 해당 블럭을 추가합니다.

물론 추가되는 대로 바로 스레드를 진행합니다.


이시점에서 로그 0이 찍힙니다.


그리고 결국 글로벌에 넣어진 0번 블럭은 첫 로그를 찍고 sleep문으로 잠들어 있는동안


dqueue에서는 1번 블럭을 실행합니다.


마찬가지로 시그널의 wait을 실행하고 시그널은 이제 2가 되었으므로 다음 코드를 실행합니다.


그래서 글로벌로 0번과 1번 블럭을 실행하고 있는 두개의 스레드가 작동합니다.


1번블럭도 sleep문으로 자고있는사이 dqueue는 2번 블럭을 실행합니다.


그리고 시그널의 wait 실행합니다. 이때 내부카운트가 이미 2이므로 이것이 줄때까지 스레드를 잠재웁니다.


그래서 2번 블럭은 진행 하다말고 정지한 상태고 dqueue는 아직 2번 블럭이 종료되지 않았으니 3번을 실행하지 않고 기다립니다.


그리고 0번과 1번은 2초후 종료 로그를 찍고 시그널의 signal을 실행합니다.


어느쪽이든 한순간 먼저 끝날때 signal에 의해 wait에서 얼어있던 스레드가 진행 됩니다. 시그널은 2에서 signal로 1이 되었다가 wait진행되면서 2번 블럭에 의해 다시 2가 됩니다. 그리고 1번 블럭이 끝날때 또한 signal을 날리기 때문에 마찬가지로 wait에서 걸려있던 3번이 다시 진행되는 겁니다.






이렇게 간단히 GCD를 다루어 보았지만 여기서 직접 스레드를 다루는 내용은 나오지 않았습니다.


작업을 큐에 넣기만 할뿐 직접 스레드를 만들어서 붙여주는 작업은 GCD가 알아서 해주기 때문이죠.


GCD는 NSOperation의 근본 개념으로 스레드를 사용하면서 작업을 분산해서 효율적으로 진행하도록 하는데 목적이 있습니다.


아마도 같은 GCD라해도 iOS는 멀티스레딩에서 끝나는 정도지만 데스크탑인 OSX에서는 멀티코어 대책으로 나온 기술이라 한 CPU에 작업이 몰리지 않도록 효율적인 제어를 하는 기술이라고 이해됩니다.


자료출처 :  http://cafe.naver.com/mcbugi?1302828470000







 



 







 

'개발 > App Developer' 카테고리의 다른 글

Bing Map iOS SDK  (0) 2011.05.10
트위터 API 관련  (1) 2010.12.23
앱 개발자 등록 관련 블로그/사이트  (0) 2010.11.17
코어 데이터 Core Data  (0) 2010.11.12
* 놈에 대한 참고 하나더  (0) 2010.11.12