본문 바로가기
프로그래밍 놀이터/iOS

[Objective-C] 병렬 프로그래밍

by 돼지왕 왕돼지 2018. 1. 10.
반응형

 [Objective-C] 병렬 프로그래밍


출처 : OS X 구조를 이해하면서 배우는 Objective-C Chap 19.

@ autorelease, @ synchronized, addDependency, addExecutionBlock, addoperation, arc, autorelease, blockOperationWithBlock, BSD, cancel, cancelAllOperations, Concurrency Programming Guide, condition lock, CPU, critical section, currentThread, DeadLock, dependencies, dependency, Detach, detachDrawingThread, detachNewThreadSelector, distributed object, executionBlocks, exit, GCD, grand central dispatch, Grand Central Dispatch Reference, GUI 앱과 스레드, initWithCondition, initWithTarget, iscancelled, isMultiThreaded, isSuspended, Join, LOCK, lockWhenCondition, Mac OS X, mainthread, maxConcurrentOperationCount, memory 공유, mutex, mutual exclusion, mutual exclusion semaphore, nsblockoperation, NSConditionLock, nsinvocationoperation, nslock, nsmutabledictionary, NSOperation, nsoperationqueue, NSOperationQueueDefaultMaxConcurrentOperationCount, NSOperationQueuePriority, NSOperationQueuePriorityHigh, NSOperationQueuePriorityLow, NSOperationQueuePriorityNormal, NSOperationQueuePriorityVeryHigh, NSOperationQueuePriorityVeryLow, nsrecursivelock, NSThread, performSelectorOnMainThread, priority, process, removeDependency, secondary thread, semaphore, setMaxConcurrentOperationCount, setqueuepriority, setsuspended, shared variable, sleepForTImeInterval, sleepUntilDate, start, subthread, task, thread, thread-safe, thread-unsafe, threadDictionary, trylock, tryLockWhenCondition, Unlock, unlockWithCondition, waitUntilAllOperationAreFinished, waituntilfinished, [Objective-C] 병렬 프로그래밍, 가상 실행 단위, 공유 변수, 교착 상태, 그랜드 센트럴 디스패치, 대기열, 락, 락 획득 시도, 멀티 스레드, 멀티스레드, 메모리 공유, 메인 스레드, 배타 제어, 상호 배제, 새로운 병렬 처리 프로그래밍, 서브 클래스, 성능, 세마포어, 스레드 세이프, 스레드 풀, 스레드의 기본 개념, 실행 스택, 애플 문서, 오퍼레이션 객체와 병렬 처리, 우선도, 은폐 영역, 의존 관계, 자동 해제 풀, 조건이 있는 락, 주의사항, 추상 클래스, 카운터 관리 방식, 커넥션을 사용한 통신, 크리티컬 섹션, 태스크, 태스크 사이에 의존 관계 설정, 태스크가 모두 끝날 떄까지 기다리기, 현재 스레드


19.1. 멀티 스레드


* 19.1.1. 스레드의 기본 개념


-

스레드(thread)란 프로세스(process)안에서 CPU 이용권을 가진 가상적인 실행 단위이다.

일반적으로 하나의 프로세스에는 하나의 스레드밖에 없지만 복수의 스레드를 생성해 프로세스 안에서 병렬로 동작시킬 수도 있다.



-

프로그램 실행이 시작될 때부터 동작하는 스레드를 메인 스레드라 하고 그 외에 나중에 생성된 스레드를 세컨더리 스레드(secondary thread) 또는 서브 스레드(subthread)라고 한다.



-

부모 스레드는 자식 스레드의 실행이 끝나길 기다렸다 합류(join)할 수 있다.

대다수의 스레드 구현은 따로 지정하지 않으면 이 방법으로 스레드를 실행한다.

한편, 스레드가 생성된 다음 부모 자식 관계를 따로 떼어내서 합류 동작을 하지 않도록 지정하기도 하는데, 이것을 디태치(detach)라고 한다.

여기서 설명하는 NSThread 는 디태치된 상태로 스레드를 생성한다.



-

생성된 스레드는 프로세스의 어드레스 공간을 공유하므로 변수에 자유롭게 접근할 수 있다.

복수의 스레드에서 접근하는 변수를 공유 변수(shared variable)라고 부른다.



-

복수의 스레드가 공유 변수에 맘대로 접근하면 변숫값이 올바르다는 걸 보증할 수 없게 된다.

이런 경우 스레드 사이에 상호 배제(배타 제어, mutual exclusion)를 실시해야 한다.



-

스레드마다 따로 실행 스택이 할당되어 독립적으로 관리된다.

다른 스레드의 스택에 있는 변수(자동 변수)에는 기본적으로 접근하면 안 된다.



-

카운터 관리 방식으로 한다면 자식 스레드에서 객체가 사라지지 않도록 부모 스레드와는 다른 자동 해제 풀을 작성해서 관리해야 한다.




* 19.1.2. 스레드 세이프


-

어떤 인스턴스를 여러 스레드가 동시에 조작해도 결괏값이 잘못되거나 인스턴스가 불안정해지지 않는다면 그 클래스는 스레드 세이프(thread-safe)라고 한다.

결과를 보증할 수 없다면 ‘스레드 세이프가 아니다’ 또는 스레드 언세이프(thread-unsafe)라고 한다.

보통 변하지 않는 객체는 스레드 세이프이고, 변경 가능한 객체는 스레드 세이프가 아니다.

변하지 않는 객체는 스레드 사이에서 안전하게 주고받을 수 있지만 변경 가능한 객체를 공유하려면 상호 배제나 동기화를 해야 한다.



-

특히 C 언어 함수는 주의가 필요하다.

BSD 함수의 대다수는 스레드 세이프하지 않다.




* 19.1.3. 주의사항



* 19.1.4. NSThread 사용해 스레드 생성


-

새로운 스레드 생성은 다음 클래스 메서드를 실행한다.


+(void) detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

 새로운 스레드를 생성해 객체 aTarget 에 대해 메서드 호출을 실행한다.

 셀렉터 aSelector 는 id 형 인수를 하나만 따서 void 를 돌려주는 액션 메서드이어야 한다.



-

지정한 메서드 실행이 끝나면 스레드 실행을 종료한다.

스레드는 처음부터 디태치되어 있으며 종료할 때 부모 스레드와 합류하지 않는다.

또한 메인 스레드가 종료되면 하위 스레드를 포함한 전체 프로그램이 종료된다.



-

카운터 관리 방식(수동 및 ARC)을 사용할 때 실행된 메서드 자체가 자동 해제 풀을 관리해야 한다.

그리고 인수 aTarget 과 anArgumnet 에 지정된 객체는 스레드 생성과 동시에 유지되어 스레드 종료와 함께 해제된다.



-

NSApplication 클래스의 다음 클래스 메서드로 스레드를 생성할 수 있다.

이 방법은 앞에서 본 메서드를 사용하는데 카운터 관리 방식이면 스레드용 자동 해제 풀도 작성한다.


+(void)detachDrawingThread:(SEL)selector toTarget:(id)target withObject:(id)argument



-

프로그램이 멀티스레드로 동작하고 있는지의 여부는 NSThread 의 클래스 메서드로 확인할 수 있다.


+(BOOL)isMultiThreaded

  복수의 스레드가 병렬로 동작하고 있는지 또는 그 순간은 메인 스레드뿐이더라도 지금까지 스레드가 생성된 적이 있다면 YES 를 돌려준다.




* 19.1.5. 현재 스레드


-

서브 스레드는 실행 도중에 종료시킬 수도 있다.

카운터 관리 방식이라면 종료 전에 자동 해제 풀을 꼭 해제시켜야 한다.


+ (void)exit


+ (NSThread*)currentThread


+ (NSThread*)mainThread



-

스레드마다 스레드 고유의 NSMutableDictionary 형식의 사전을 가질 수 있다.

사전은 NSThread 인스턴스에 다음 메시지를 보내서 취득한다.


-(NSMutableDictionary*)threadDictionary



-

몇 초간 현재 스레드의 실행을 일시적으로 중단시킬 수 있다.


+ (void)sleepForTImeInterval:(NSTimeInterval)ti



-

지정한 시각이 될 때까지 스레드 실행을 중단시키는 클래스 메서드도 있다.


+ (void)sleepUntilDate:(NSDate*)aDate




* 19.1.6. GUI 앱과 스레드


-

-(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait

 셀렉터 aSelector 와 인수 arg 로 지정한 메서드 실행을 메인 스레드에 의뢰한다.

 wait 가 YES 라면 실행을 완료할 때까지 현재 스레드는 기다린다.

 메인 스레드에 이벤트 반복(실행 반복)이 반드시 있어야 한다.




19.2. 상호 배제


* 19.2.1. 상호 배제가 필요한 예


-

동시에 하나의 스레드만 점유해서 실행하기 원하는 코드 부분을 크리티컬 섹션(critical section) 또는 은폐 영역이라 부른다.

상호 배제의 목적은 크리티컬 섹션을 실행할 수 있는 스레드를 제어하기 위함이다.




* 19.2.2. 락


-

공유 리소스의 상호 배제를 위해 NSLock 클래스를 이용한다.

이 클래스의 인스턴스는 복수 스레드 동작을 조절하는 세마포어(semaphore)또는 mutex(mutual exclusion semaphore)로 동작한다.

Cocoa 환경에서는 이것을 락(lock)이라고 부른다.



-

락은 한 번에 하나의 스레드만 “획득”해서 “시용 중”으로 바꿀 수 있다.

락을 획득하는 것을 “락을 건다”, 획득한 락을 해제하는 것을 “언락한다”고 한다.



-

alloc 과 초기자 init 조합으로 생성, 초기화할 수 있다.

하지만 락은 프로그램이 멀티스레드로 동작하기 전에 생성해야 한다.

- (void) lock

- (void) unlock



-

lock 을 걸면 그 후에는 반드시 unlock 을 한 번만 실행해야 한다.

lock 에 대응하는 unlock 을 하는 건 같은 스레드여야 한다.




* 19.2.3. 교착 상태


-

락을 잘못 쓰면 생각과는 달리 상호배제가 안 되던지 때에 따라서는 여러 스레드가 동작 불능에 빠지는 교착 상태(deadlock)가 발생하기도 한다.

교착 상태는 복수의 스레드(또는 프로세스)가 실행 불가능한 상태로 영원히 기다리는 것이다.




* 19.2.4. 락 획득 시도


-

NSLock 에는 락 획득, 해제 외에도 락을 획득할 수 있는지를 확인하는 기능이 있다.


- (BOOL) tryLock

 리시버인 락을 획득할 수 있는지 확인해보고 취득하면 YES 를 돌려준다.

 락을 획득하지 못했을 때는 lock 처럼 잠자기 상태에 들어가는 것이 아니라 NO 를 돌려주고 그대로 계속 실행한다.




* 19.2.5. 조건이 있는 락


-

NSConditionLock 은 조건부 락(condition lock)이다.

이 락은 값(정숫값)을 가지고 그 값에 따라 락을 획득하거나 기다리게 한다.


- (id) initWithCondition:(NSInteger)condition


- (NSInteger) condition


- (void) lockWhenCondition:(NSInteger)condition

 락이 사용 중이라면 스레드는 잠자기 상태에 들어간다.

 사용 중이 아닐 경우 락의 값과 인수 condition 값이 일치하면 락을 사용 중으로 바꾸고 계속 실행하지만 일치하지 않으면 스레드는 잠자기 상태가 된다.


- (void) unlockWithCondition:(NSInteger)condition


- (BOOL) tryLockWhenCondition:(NSInteger)condition



-

NSConditionLock 인스턴스의 상태는 정숫값이지만 이것은 열거형 상수나 매크로로 정의하는게 좋다.

단순히 0이나 1 같은 값으로 하면 알기도 어렵고 실수하기도 쉽다.




* 19.2.6. NSRecursiveLock


-

NSLock 클래스의 락은 일단 락을 획득한 스레드가 락 해제 없이 같은 락을 다시 획득하려고 하면 스레드가 잠자기 상태에 들어간다. 하지만 잠자기 상태에서 깨울 쓰레드가 없으므로 교착 상태에 빠진다.



-

NSRecursiveLock 는 락을 획득한 스레드가 몇 번이나 같은 락을 획득하더라도 교착 상태에 빠지지 않는다.

하지만 다른 스레드는 당연히 이 락을 획득하지 못한다.

획득 횟수와 해제 횟수가 같아졌을 때 락은 해제된다.



-

NSRecursiveLock 클래스의 락은 편리하지만 반복해서 락이 되는 상황을 제외하고는 NSLock  을 사용하는 쪽이 성능이 좋은 편이다.




* 19.2.7. @synchronized


-

@synchronized 컴파일러 지시자를 사용해 락을 잡을 수 있다.

@synchronized(obj){

     // do something...

}


이렇게 하면 런타임 시스템이 블록을 독점적으로 수행하는 락(mutex)을 생성한다.

인수 obj 는 일반적으로 블록에서 상호 배제의 대상으로 보호하고 싶은 객체를 지정한다.

obj 자체는 락 객체가 아니어도 된다.



-

블록 안에서 외부로 break 나 return 으로 빠져나왔을 때도 블록 실행이 종료된 것으로 본다.

또한 블록 안에서 예외가 발생하면 런타임 시스템은 예외를 포착하여 락을 해제한다.



-

@synchronized 인수 객체에 대응하는 락은 정해져 있으므로 같은 객체를 인수로 하는 @synchronized 블록이 여러 곳에 있을 때 동시에 실행되면 안 된다.



-

@synchronized 는 앞에서 설명한 NSRecursiveLock 락처럼 재귀적으로 사용할 수 있다.



-

@synchronized 블록을 사용하면 락 획득과 해제가 반드시 대응해서 한 번씩 일어나므로 일반락을 사용할 때 자주 발생하는 실수로, 해제하지 않고 넘어가는 실수를 막을 수 있다.

일반 락과 비교하면 다소 복잡한 병렬 알고리즘을 작성하기에는 어렵지만 대부분 상호 배제를 알기 쉽게 설명할 수 있다.






19.3. 오퍼레이션 객체와 병렬 처리


* 19.3.1. 새로운 병렬 처리 프로그래밍


-

그랜드 센트럴 디스패치(GCD : Grand Central Dispatch).

GCD 의 핵심은 C 언어로 작성된 시스템 서비스로, 앱은 해야 할 각각의 작업을 블록객체로 작성하여 대기열에 넣는다.



-

GCD 의 기능을 직접 이용하려면 C 언어의 함수를 이용해야 하지만 Objective-C 에서는 작업을 NSOperation 클래스를 사용해 나타내고 NSOperationQueue 클래스의 대기열에 추가해서 병렬 처리를 구현할 수 있다.

자세한 내용은 ‘Concurrency Programming Guide’, ‘Grand Central Dispatch Reference’ 같은 문서를 참고하라.



-

실행해야 할 작업의 한 묶음을 추상적으로 태스크라고 부른다.




* 19.3.2. NSOperation 을 사용한 처리 개요


-

NSOperation 인스턴스를 오퍼레이션 객체(operation object) 또는 단순히 오퍼레이션이라고 부른다.

오퍼레이션은 일반적으로 직접 실행되는 것이 아니라 대기열에 들어 있는 순서대로 처리된다.

스레드는 차례차례 계속 생성되는 것이 아니라 몇 개의 스레드가 준비되어 있어 손이 빈 스레드에 오퍼레이션을 맡긴다.

이런 스레드 관리 방법을 스레드 풀이라고 한다.



-

GCD 는 하드웨어나 그 때의 시스템 전체의 부하 같은 조건에서 적당한 스레드 수를 정한다.

각 오퍼레이션이 어느 스레드에서 실행되는지, 어떻게 스케줄링 되는지는 프로그래머가 알 필요가 없다.



-

오퍼레이션은 대기열에 들어간 순서대로 실행되며 ‘태스크 A가 끝나면 태스크 B를 수행한다’라는 관계를 지정할 수 있다.

이 때 태스크 B 는 태스크 A 에 의존한다고 하며, 이런 관계를 의존 관계(dependency)라고 한다.

또한 오퍼레이션에는 우선도(priority)를 설정할 수도 있다.



-

오퍼레이션에 대기열 기능을 제공하는 것이 NSOperationQueue 이다.

이 클래스 인스턴스를 오퍼레이션 큐 또는 간단히 큐라고 부른다.

오퍼레이션을 일단 큐에 넣으면 그것이 어떻게 실행될지는 시스템이 정한다.

하지만 큐에 들어간 오퍼레이션을 실행하기 전에 취소할 수 있다.



-

오퍼레이션 객체에 start 메서드를 직접 호출하는 것으로도 태스크는 실행시킬 수 있다.

하지만 태스크 병렬 처리를 위해 start 안에 스레드를 스스로 시작하는 건 표준 구현 방법이다.



-

오퍼레이션 정의 방법 같은 자세한 내용은 NSOperation 클래스의 레퍼런스나 ‘Concurrency Programming Guide’ 를 참고하라.




* 19.3.3. NSOperation 과 NSOperationQueue 의 간단한 사용 방법


-

NSOperation 은 태스크를 처리하는 기능을 제공하지만, 그 자체는 추상 클래스이다.

따라서 서브 클래스를 정의해서 해야 할 처리는 그 안에 작성해야 한다.

하지만 오퍼레이션 객체는 여러 스레드에서 동시에 접근할 가능성이 있으므로 서브 클래스의 메서드는 스레드 세이프가 되도록 작성해야 한다.



-

NSOperation 의 서브 클래스에서는 실행할 태스크를 main 메서드에 작성해야 한다.

NSOperation 에서 정의된 메서드는 아무것도 하지 않기 때문에 super 를 호출하지 않아도 된다.

- (void) main{

     @try{

          @autorelease{

               // do something..

          }

     } @catch( … )

          // @throw 등으로 외부로 전파해선 안된다.

     }

}



-

NSOperation 의 지정 초기자는 init 이므로 서브 클래스의 초기자에서는 반드시 init 을 호출해야 한다.



-

오퍼레이션 객체를 큐에 추가하려면


-(void) addOperation:(NSOperation*)operation



-

하나의 프로그램에서 큐는 여러 개 만들 수 이다.

그러나 어떤 오퍼레이션 객체는 한 번에 하나의 큐에만 들어간다.

또한 이미 실행이 끝났거나 실행 중인 오퍼레이션 객체는 큐에 들어갈 수 없으며 만약 그렇게 하면 예외가 발생한다.



-

일단 큐에 들어간 오퍼레이션 객체는 큐에서 제거할 수 없다.

그 작업을 실행하고 싶지 않다면 처리를 취소해야 한다.




* 19.3.4. 태스크가 모두 끝날 떄까지 기다리기


-

NSOperationQueue 의 다음 메서드를 사용하면 큐에 들어간 작업이 모두 끝날 때까지 현재 스레드를 블록할 수 있다.


- (void) waitUntilAllOperationAreFinished

- (void) addOperations:(NSArray*)ops waitUntilFinished:(BOOL)wait




* 19.3.5. 오퍼레이션 객체를 사용한 간단한 예




* 19.3.6. NSInvocationOperation 사용 방법


-

Cocoa 프레임워크에는 NSOperation 의 서브 클래스로, NSInvocationOperation 과 NSBlockOperation 이 있다.

이런 클래스를 사용하면 서브 클래스를 정의하지 않아도 오퍼레이션 객체를 생성할 수 있다.



-

NSInvocationOperation 에는 다음 초기자가 있는데 대상이 되는 객체에 메시지 송신을 태스크로 실행하는 오퍼레이션 객체를 돌려준다.

태스크 내용은 미리 메서드로 정의되어 있을 때 이 메서드가 유용하다.


- (id) initWithTarget:(id)target selector:(SEL)sel object:(id)arg




* 19.3.7. NSBlockOperation 사용 방법


+(id)blockOperationWithBlock:(void (^)(void))block




* 19.3.8. NSBlockOperation 에 여러 블록 객체 추가


-

여러 블록 객체가 있을 경우 병렬로 실행된다.


- (void) addExecutionBlock:(void (^)(void))block

- (NSArray*)executionBlocks




* 19.3.9. 태스크 사이에 의존 관계 설정


-

NSOperation 에 정의되어 있는 의존 관계 설정, 해제 관련 메서드들이다.


- (void) addDependency: (NSOperation*)operation

- (void) removeDependency:(NSOperation*)operation

- (NSArray*)dependencies



-

의존 관계에는 화살표가 닫힌 경로를 구성하면 안 된다.

즉, 화살표가 가리키는 방향을 따라가다 원래의 장소에 돌아오는 관계가 있으면 안 된다.

그런 관계가 있는 오퍼레이션은 어느 쪽도 실행되지 않으므로 처리가 끝나지 않게 된다.




* 19.3.10. 태스크에 우선 순위 설정하기


-

우선순위를 높게 지정한 태스크가 우선순위가 낮은 태스크보다 반드시 먼저 실행된다는 보장은 없다.


- (void) setQueuePriority:(NSOperationQueuePriority)priority

- (NSOperationQueuePriority) queuePriority



-

NSOperationQueuePriority 는 정수형이지만 다음처럼 정의된 상수를 사용한다.


enum{

     NSOperationQueuePriorityVeryLow = -8,

     NSOperationQueuePriorityLow = -4,

     NSOperationQueuePriorityNormal = 0,

     NSOperationQueuePriorityHigh = 4,

     NSOperationQueuePriorityVeryHigh = 8,

};

typedef NSInteger NSOperationQueuePriority;




* 19.3.11. 병렬로 동작하는 태스크의 최대수 설정하기


-

오퍼레이션 큐가 여러 개인 태스크를 동시에 실행할 때 최대 몇 개의 태스크를 병렬로 동작시킬 수 있는지 설정할 수 있다.


- (void) setMaxConcurrentOperationCount:(NSInteger)count

- (NSInteger) maxConcurrentOperationCount



-

일반적으로 태스크 병렬 동작은 시스템 상황을 보고 적절히 판단한다.

이 상태를 선택하려면 상숫값 NSOperationQueueDefaultMaxConcurrentOperationCount 를 지정한다.

이 값이 기본값이다. ( 실제값은 -1 )


최솟값으로 1을 설정해도 반드시 큐에 들어온 순서대로 태스크가 처리되는 건 아니다.

또한 큰 숫자를 지정해도 동작이 빨라지는 것이 보장되지 않는다.




* 19.3.12. 태스크 중지하기


-

- (void) cancel

- (BOOL) isCancelled

- (void) cancelAllOperations


태스크 실행 전에 cancel 을 받아서 isCancelled 가 YES 를 돌려주면 오퍼레이션 큐는 이 오퍼레이션 객체를 실행하지 않고 제거한다.



-

태스크 실행 중에 cancel 을 받았을 때 중단처리로 이동하기 위해서는 태스크 내용을 그렇게 프로그래밍해야 한다.

구체적으로 main 메서드의 앞부분 및 처리 도중의 각 요소마다 자기자신의 isCancelled 값을 확인해서 YES 가 돌아오면 중단 처리를 하도록 한다.




* 19.3.13. 큐 스케줄링을 중단 상태로 만들기


-

어떤 오퍼레이션 큐의 동작을 일시 중지시켜 큐에 있는 오퍼레이션을 실행하지 않을 수도 있다.


-(void) setSuspended: (BOOL)suspend

-(BOOL) isSuspended




19.4. 병렬처리의 예제 프로그램




19.5. 커넥션을 사용한 통신

-

Mac OS X 의 Foundation 프레임워크에는 다른 스레드 또는 다른 프로세스를 엮어서 쌍방향 통신로로 NSConnection 클래스가 있다.

NSConnection 객체는 스레드 사이의 스레드 세이프한 통신로로 이용이 가능하다는 것 외에도 앱 사이에 사용되는 분산 객체(distributed object)를 작성하는 수단을 제공한다.

하지만 iOS 에서는 이 클래스를 제공하지 않는다.



-

최근 애플 문서에는 새로운 앱을 작성할 때 스레드를 사용해서 효율을 높이려면 앞에서 설명한 오퍼레이션 객체나 GCD 를 이용하는 편이 좋다고 되어 있다.

하지만 Mac OSX 에서 앱 사이를 연계할 때나 역할 분담이 명확한 스레드 사이에 통신할 때는 무척 유용하다.





반응형

'프로그래밍 놀이터 > iOS' 카테고리의 다른 글

[Objective-C] 기타  (0) 2018.01.12
[Objective-C] 키-값 코딩  (0) 2018.01.11
[Objective-C] 예외와 에러  (0) 2018.01.09
[Objective-C] 어플리케이션 구조  (0) 2018.01.08
[Objective-C] 메시지 송신 패턴  (0) 2018.01.07

댓글