Thread 대용

    Thread는 concurrency를 지원하는 방법 중 상대적으로 low-level이며 복잡한 방법이다. thread를 사용할 때 설계에 만전을 기하지 않는다면 동기화와 타이밍 문제에 직면하게 될 것이다. 또 thread를 굳이 써야할 정도의 task인지를 확실하게 정의해야 한다. thread는 CPU와 메모리에 어마어마한 overhead를 부여하게된다. 굳이 이런 overhead를 감내할 필요가 없다면 아래 설명하는 thread의 대용품들을 사용하자.



-Operation objects

    NSOperation & NSOperationQueue


-Grand Central Dispatch (GCD)

    Grand Central Dispatch


-Idle-time notifications

    NSNotificationQueue에 Notification을 넣고 idle-time 옵션을 준다. 해당 NSNotificationQueue는 run loop가 idle 상태가 되었을 때, notification을 전달하여 수행한다.


-Asynchronous functions

    Cocoa API상에서 함수 자체가 asynchronous excution을 하게 설계된 것들이 있다. 이들을 thread 대용으로 활용할 수 있다.


-Timers

    NSTimer나 CFRunLoopTimer를 사용하면 thread를 사용하기에 너무 하찮은 작업들을 주기적으로 반복 동작 시킬 수 있다.


-Separate processes






Thread 구현


Cocoa threads

    NSThread 클래스를 통하여 구현한다. 또는 NSObject 클래스에서 'performSelectorInBackground:withObject:' 메소드를 사용하여 새로운 thread에 실행 로직을 부여할 수 있다.


POSIX threads

    POSIX thread는 thread를 생성하기 위한 C기반의 인터페이스이다. Cocoa application을 구현하지 않는다면 POSIX thread가 thread를 사용할 수 있는 가장 손쉬운 방법이 될 것이다.


Multiprocessing Services

    이는 오래된 방식으로 OS X에서만 사용 가능하다. 



*POSIX (Portable Operating System Interface) : 서로 다른 UNIX OS의 공통 API를 정리하여 이식성 높은 유닉스 응용 프로그램을 개발하기 위한 목적으로 IEEE가 책정한 애플리케이션 인터페이스 규격.




Inter-thread Communication


Direct messaging

    다른 thread에게 seletor를 수행하게 만들 수 있다. 이 방식을 통하여 해당 thread에 메시지를 전달할 수 있다.


Global variables, Shared memory & objects

    thread 사이에 전역 변수, 공유 객체, 메모리 shared block을 이용하여 정보를 교환할 수 있다. 이 방법은 다른 통신 방법에 비해 단순하고 빠르지만 동기화 문제에 대한 무결성을 보장해야 한다. 이를 보장하지 못할 경우엔 race condition, 데이터 오류, crash 등의 문제를 발생시킬 수 있다.


Conditions

    condition은 thread가 코드의 일부분을 제어하기 위한 동기화 수단이다. 


Run loop sources

    =업데이트 예정=


Ports & sockets

    =업데이트 예정=


Message queues

    데이터를 넣고 빼기 쉬운 추상적 구조의 큐이다. 메시지 큐는 단순하고 편리하지만 다른 communication에 비해 효율적이지 않다.


Cocoa distributed objects

    =업데이트 예정=




Design Tips


-Thread의 직접 생성을 피해라

-Thread를 계속 일하게 만들어라

-공유 데이터 구조를 피해라

-User Interface는 main-thread에서 관리해라

-예외 처리를 명확하게 해라

-Thread를 없앨 때는 명확하게 해라

-Thread-safe를 보장하는 라이브러리를 사용해라


'Programming > Mac & iOS' 카테고리의 다른 글

[iOS] App Extensions in iOS  (0) 2016.04.30
[iOS] Automatic Retain Counting (ARC) in Objective-C & Swift  (0) 2016.04.28
[iOS] 스레드(Thread) 사용법 & Tips  (0) 2016.04.24
[iOS] NULL, nil, Nil, NSNull  (0) 2016.04.21
[iOS] self와 _의 차이  (0) 2016.04.21
[iOS] KVC, KVO  (0) 2016.04.19

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의 놀라운 마술을 볼 수 있다.
https://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/


안드로이드와 iOS의 스레드 사용법은 완전히 다르다.


NSThread *t1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunc:) object:nil];

[t1 start];


 위와 같이 스레드를 생성해서 이용하면 된다. threadFunc로 스레드의 역할을 정의해주면 된다. 자바의 Thread의 run()이라고 생각하면 이해가 쉽다. 스레드를 종료시킬 때는 스레드를 강재로 죽이는 것보다는 스레드의 실행단위가 완료될 수 있도록 간접적으로 죽이는 방법이 전반적으로 선호된다. 특히 iOS는 시스템의 안전성을 위해서 간접적으로 죽이는 방법만 허용한다.


[t1 cancel];


 cancel을 사용하여 내부의 flag 값을 바꾸어 종료 시키는 방법만 허용 시킨다. 단, threadFunc 내부에서 밑에와 같은 스레드의 종료를 확인하는 명령어 한 줄을 추가 해준다.


if([[NSThread currentThread] isCancelled]  == YES);


 안드로이드 정책 상으로 UI 조작 등은 UI스레드(메인 스레드:자바)만 할 수 있도록 막아놓았다. 이는 여러 스레드들이 동시에 UI에 접근하여 초래하는 혼란을 방지하기 위함이다. 그래서 개발자가 임의로 만든 스레드로 UI를 조작하는 상황이 발생하게 되면 이를 UI스레드에게 알려주어야 한다. 이 때, 핸들러와 루퍼를 이용하면 된다.



1. 핸들러

 핸들러는 스레드로 부터 메시지를 받아서 처리하는 부분이다. 핸들러 내부적으로 메시지 큐를 가지고 있어서 다른 스레드로부터 온 메시지들을 메시지 큐에서 하나씩 꺼내면서 처리하게 된다.


tHandler = new Handler(){


@Override

public void handleMessage(Message msg) {

// TODO Auto-generated method stub

viewText(msg.arg1 + " System Time ");

}

};


 위의 코드는 메시지 큐의 메시지를 처리하는 handleMessage 메소드를 오버라이딩하여 인자들을 TextView에 출력하는 예제이다. 그럼 메시지 큐에 메시지를 넣어줘야하는데 이는 루퍼를 이용하면 된다.


그리고 Activity에서 onDestroy()를 호출할 때, 루퍼를 종료시키는 것을 까먹지 말자.


@Override

protected void onDestroy() {

// TODO Auto-generated method stub


if(tHandler != null)

tHandler.getLooper().quit();


tThread.setRun(false);


super.onDestroy();

}




2. 루퍼

 루퍼는 스레드에서 처리한 내용을 메시지 큐에 삽입하여 핸들러에서 처리할 수 있도록 해준다. N개의 스레드가 메시지를 보내면 N개의 루퍼가 필요하다.



public class TimeThread extends Thread{


boolean isRun;



public void setRun(boolean isRun){

this.isRun = isRun;

}


public void run() {

// TODO Auto-generated method stub

this.isRun = true;


Looper.prepare();


while(isRun){

toActivity = tHandler.obtainMessage();


toActivity.arg1System.currentTimeMillis();


tHandler.sendMessage(toActivity);


try {

TimeThread.sleep(100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}


Looper.loop();

}


}



 위의 스레드는 100 밀리초마다 시스템 시간을 인자로 넘겨주는 예제이다. Looper.prepare()와 Looper.loop() 사이에 행위와 핸들러로 메시지를 보내주는 내용을 정의하면된다.

+ Recent posts