[Objective-C] 병렬 프로그래밍
출처 : OS X 구조를 이해하면서 배우는 Objective-C Chap 19.
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 에서 앱 사이를 연계할 때나 역할 분담이 명확한 스레드 사이에 통신할 때는 무척 유용하다.
다음 글 : [Objective-C] 키-값 코딩
'프로그래밍 놀이터 > 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 |
댓글