Cocoa Operation은 비동기적으로 수행하려는 작업을 캡슐화하는 객체 지향 방식이다. Operation은 Operation Queue와 함께 사용하거나 단독으로 사용하도록 설계되었다. Operation은 Objective-C 기반이므로 OS X 및 iOS의 Cocoa 기반 애플리케이션에서 가장 일반적으로 사용된다.
이번 챕터에서는 Operation을 정의하고 사용하는 방법에 대해 설명한다.
About Operation Objects
operation object는 애플리케이션에서 수행 할 작업을 캡슐화하는 데 사용하는 NSOperation 클래스 (Foundation 프레임워크에서)의 인스턴스이다. NSOperation 클래스 자체는 유용한 작업을 수행하기 위해 상속을 해줘야 하는 추상 클래스이다. 이 클래스는 추상적 임에도 불구하고 상당한 양의 인프라를 제공하여 서브 클래스에서 수행해야 하는 작업의 양을 최소화한다. 또한 Foundation 프레임워크는 기존 코드와 동일하게 사용할 수 있는 두 가지 구체적인 서브 클래스를 제공한다. 하단의 표에는 이러한 클래스와 각 클래스를 사용하는 방법이 요약되어 있다.
클래스 | 설명 |
NSInvocationOperation | 이 클래스를 있는 그대로 사용하여 애플리케이션의 오브젝트 및 셀렉터를 기반으로 opertaion object를 만든다. 이미 필요한 태스크를 수행하는 메서드가 존재하는 경우 이 클래스를 사용할 수 있다. 상속이 필요하지 않기 때문에 이 클래스를 사용하여 보다 동적인 방식으로 operation object를 만들 수도 있다. |
NSBlockOperation | 이 클래스를 있는 그대로 사용하여 하나 이상의 블록 객체를 동시에 실행한다. 둘 이상의 블록을 실행할 수 있기 때문에, block operation object는 group sematic을 사용하여 작동한다. 관련된 모든 블록의 실행이 완료된 경우에만 operation object가 완료된 것으로 간주된다. |
NSOperation | custom operation object를 정의하는 기본 클래스이다. NSOperation을 상속하면 operation이 실행되고 상태를 보고하는 기본 방식을 변경하는 기능을 포함하여 자체 operation의 구현을 완벽하게 제어 할 수 있다. |
모든 operation object는 다음과 같은 주요 기능을 제공한다.
- operation object 간의 그래프 기반 종속성 설정을 제공한다. 이러한 종속성은 종속된 모든 operation의 실행이 완료 될 때까지 지정된 operation이 실행되지 않도록 한다.
- operation의 메인 태스크가 완료된 후 실행되는 optional completion block을 제공한다.(OS X v10.6 이상에만 해당)
- KVO notification을 사용하여 operation의 실행 상태에 대한 변경 사항을 모니터링 하는 것을 제공한다.
- operation의 우선 순위를 지정하여 상대적 실행 순서에 영향을 줄 수 있도록 제공한다.
- 실행하는 동안 operation을 중지 할 수 있는 canceling semantics를 제공한다.
Operation은 애플리케이션의 동시성 수준을 개선하는 데 도움이 되도록 설계되었다. 또한 Operation은 애플리케이션의 동작을 쉽게 개별적으로 구성하고 캡슐화하는 좋은 방법이다. 애플리케이션의 메인 스레드에서 일부 코드를 실행하는 대신 하나 이상의 operation object를 큐에 보내고 해당 operation이 하나 이상의 분리 된 스레드에서 비동기 방식으로 수행되도록 할 수 있다.
Concurrent Versus Non-concurrent Operations
일반적으로 operation을 operation queue에 추가하여 실행하지만 그렇게 할 필요는 없다. start 메서드를 호출하여 operation object를 수동으로 실행할 수도 있다. 하지만 이렇게 하면 operation이 코드의 나머지 부분과 동시에 실행되도록 보장하지는 않는다. NSOperation 클래스의 isConcurrent 메서드는 start 메서드가 호출된 스레드에 대해 작업이 동기적으로 실행되는지 비동기적으로 실행되는지를 알려준다. 기본적으로 이 메서드는 NO를 반환하고, 이것은 operation이 호출 스레드에서 동기적으로 실행됨을 의미한다.
concurrent operation, 즉 호출 스레드와 관련하여 비동기적으로 실행되는 operation을 구현하려면 operation을 비동기적으로 실행하는 추가 코드를 작성해야 한다. 예를 들어, 별도의 스레드를 생성하거나 비동기 시스템 함수를 호출하거나 다른 작업을 수행하여 start 메서드가 operation을 실행하고 operation이 완료되기 전에 즉시 반환되도록 할 수 있다.
대부분의 개발자는 concurrent operation object를 구현할 필요가 없다. operation을 항상 operation queue에 추가할 경우 concurrent operation를 구현할 필요가 없다. nonconcurrent operation을 operation queue에 보내면 큐 자체에서 operation을 실행할 스레드를 생성한다. 따라서 operation queue에 nonconcurrent operation을 추가해도 operation object 코드가 비동기적으로 실행된다. concurrent operation을 정의하는 기능은 operation을 operation queue에 추가하지 않고 비동기적으로 실행해야 하는 경우에만 필요하다.
Creating an NSInvocationOperation Object
NSInvocationOperation 클래스는 NSOperation의 구체적인 서브 클래스로, 실행시 지정한 object에 대해 지정한 selector를 호출한다. 이 클래스를 사용하면 애플리케이션의 각 태스크에 대해 수 많은 custom operation objects를 정의하는 것을 피할 수 있다. 특히 기존 애플리케이션을 수정하고 있고 필수적인 태스크를 수행하는 데 필요한 오브젝트와 메서드가 이미있는 경우 이 클래스를 사용할 수 있다. 또한 상황에 따라 호출하고자 하는 메서드가 변경 될 때도 사용할 수 있다. 예를 들어, invocation operation을 사용하여 사용자 입력에 따라 동적으로 선택되는 selector를 실행 할 수 있다. ㅇ invocation operation을 만드는 프로세스는 간단하다. 클래스의 새 인스턴스를 만들고 초기화할 때, 실행할 원하는 오브젝트와 selector를 초기화 메서드에 전달하면 된다. 하단의 코드는 생성 프로세스를 나태내는 커스텀 클래스의 두 가지 메서드를 보여준다. taskWithData 메서드는 새 invocation object가 생성되어 태스크 구현이 포함된 다른 이름의 메스드를 제공한다.
@implementation MyCustomClass
- (NSOperation*)taskWithData:(id)data {
NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTaskMethod:) object:data];
return theOp;
}
// This is the method that does the actual work of the task.
- (void)myTaskMethod:(id)data {
// Perform the task.
}
@end
Creating an NSBlockOperation Object
NSBlockOperation 클래스는 하나 이상의 블록 개체에 대한 wrapper 역할을 하는 NSOperation의 구체적인 서브 클래스이다. 이 클래스는 이미 operation queue를 사용하고 있으며 dispatch queue도 생성하지 않으려는 애플리케이션을 위한 object-oriented wrapper를 제공한다. 또한 블록 작업을 사용하면 operation 종속성, KVO notifications 및 dispatch queue에서 사용할 수 없는 기타 기능을 활용할 수 있다.
block operation을 생성 할 때 일반적으로 초기화 때 하나 이상의 블록을 추가하고, 필요에 따라 나중에 더 많은 블록을 추가 할 수 있다. NSBlockOperation 오브젝트를 실행할 때가 되면 오브젝트는 모든 블록을 default-priority를 가지는 concurrent dispatch queue에 보낸다. 그런 다음 오브젝트는 모든 블록이 실행을 마칠 때까지 기다린다. 마지막 블록이 실행을 마치면 operation object가 완료된 것으로 표시한다. 따라서 스레드 조인을 사용하여 여러 스레드의 결과를 병합하는 것처럼 block operation을 사용하여 실행 블록 그룹을 따라갈 수 있다. 차이점은 block operation 자체가 별도의 스레드에서 실행되기 때문에 애플리케이션의 다른 스레드가 block operation이 완료되기를 기다리는 동안 작업을 계속할 수 있다는 것이다.
하단의 코드는 NSBlockOperation object를 생성하는 간단한 예를 보여준다. 블록 자체에는 파라미터와 의미있는 반환 결과가 없다.
NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Beginning operation.\n");
// Do some work.
}];
block operation object를 생성한 후, addExecutionBlock 메서드를 사용하여 더 많은 블록을 추가할 수 있다. 블록을 직렬적으로 실행해야 하는 경우 원하는 dispatch queue로 직접 전달해야 한다.
Defining a Custom Operation Object
block operation과 invocation operation object가 애플리케이션의 요구 사항을 충족하지 않는 경우 NSOperation을 직접 상속받고 필요한 동작을 추가할 수 있다. NSOperation 클래스는 모든 operation object에 대한 일반적인 subclassing point를 제공한다. 또한 이 클래스는 종속성 및 KVO notification에 필요한 대부분의 작업을 처리하기 위해 상당한 양의 인프라를 제공한다. 그러나 여전히 operation이 올바르게 작동하는지 확인하기 위해 기존 인프라를 보완해야 하는 경우가 있을 수 있다. 수행해야 하는 추가 작업의 양은 nonconcurrent operation을 구현하는지 concurrent operation을 구현하는지 여부에 따라 다르다.
nonconcurrent operation을 정의하는 것은 concurrent operation을 정의하는 것보다 훨씬 간단하다.
nonconcurrent operation의 경우 메인 태스크를 수행하고 취소 이벤트에 적절히 응답하기만 하면 된다. 기존 클래스 인프라는 다른 모든 작업을 수행하면 된다.
concurrent operation의 경우 기존 인프라 중 일부를 custom code로 바꿔야한다. 다음 섹션에서는 두 가지 유형의 객체를 모두 구현하는 방법을 보여준다.
Performing the Main Task
모든 operation object는 최소한 다음 메서드를 구현해야 한다.
- A custom initialization method
- main
operation object를 known state로 만들려면 custom initialization method가 필요하고 태스크를 수행하려면 custom main method가 필요하다. 물론 필요에 따라 다음과 같은 추가 메서드를 구현할 수도 있다.
- 구현된 main 메서드에서 호출하려는 custom method
- 데이터 값을 설정하고 operation의 결과에 액세스 하기 위한 accessor methods
- NSCoding protocol의 메서드를 사용하여 operation object를 archive하거나 unarchive할 수 있다.
하단의 코드는 custom NSOperation subclass에 대한 시작 템플릿을 보여준다.(이 코드에는 일반적으로 사용하는 방법이 나와있고 취소 처리 방법은 나와 있지 않다.) 이 클래스의 initialization 메서드는 단일 오브젝트를 데이터 매개 변수로 사용하고 operation object 내부에서 참조한다. main 메서드는 결과를 애플리케이션으로 다시 반환하기 전에 해당 데이터 개체에서 표면적으로 작동한다.
@interface MyNonConcurrentOperation : NSOperation
@property id (strong) myData;
-(id)initWithData:(id)data;
@end
@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
if (self = [super init])
myData = data;
return self;
}
-(void)main {
@try {
// Do some work on myData and report the results.
}
@catch(...) {
// Do not rethrow exceptions.
}
}
@end
Responding to Cancellation Events
operation 실행이 시작된 후에는 operation이 완료 될 때까지 또는 코드에서 명시적으로 operation을 취소 할 때까지 태스크를 계속 수행한다. 취소는 operation 실행이 시작되기 전이라도 언제든지 발생할 수 있다. NSOperation 클래스는 클라이언트가 작업을 취소 할 수 있는 방법을 제공하지만 취소 이벤트를 인식하는 것은 당연히 클라이언트가 해야 한다. operation이 완전히 종료되면 할당 된 리소스를 회수 할 방법이 없을 수 있다. 따라서 operation object는 취소 이벤트를 확인하고 operation 중간에 발생하면 정상적으로 종료해야 한다.
operation object에서 취소를 지원하려면 custom code에서 주기적으로 오브젝트의 isCancelled 메서드를 호출하고 YES를 반환하는 경우 즉시 오브젝트를 반환하기만 하면 된다. 취소를 지원하는 것은 operation을 실행하는 중이거나, NSOperation를 상속받아서 사용하거나, NSOperation의 서브 클래스 중 하나를 사용하는지에 관계없이 중요하다. isCancelled 메서드 자체는 매우 가볍고 성능 저하 없이 자주 호출 할 수 있다. operation object를 설계 할 때 코드의 다음 위치에서 isCancelled 메서드를 호출하는 것을 고려해야 한다.
- 실제 작업을 수행하기 전
- 반복문에서 반복하는 동안 적어도 한 번 또는 반복문이 상대적으로 긴 경우 더 자주
- 코드에서 operation을 비교적 쉽게 중단할 수 있는 모든 지점
하단의 코드는 operation object의 main 메서드에서 취소 이벤드에 응답하는 방법에 대한 매우 간단한 예를 보여준다. 이 경우 isCancelled 메서드는 while 반복문을 통해 매번 호출되므로 작업을 시작되기 전에 빠르게 종료가 가능하고 일정한 간격으로 다시 종료할 수 있다.
- (void)main {
@try {
BOOL isDone = NO;
while (![self isCancelled] && !isDone) {
// Do some work and set isDone to YES when finished
}
}
@catch(...) {
// Do not rethrow exceptions.
}
}
Configuring Operations for Concurrent Execution
Operation object는 기본적으로 동기 방식으로 실행된다. 즉, start 메서드를 호출하는 스레드에서 작업을 수행한다. 그러나 operation queue는 nonconcurrent operations을 위한 스레드를 제공하기 때문에 대부분의 operation은 여전히 비동기적으로 실행된다. 그러나 작업을 수동으로 실행하려는 경우에도 여전히 비동기적으로 실행되도록 하려면 적절한 조치를 취해야 한다. Operation object를 concurrent operation으로 정의하여 이를 수행하라.
하단의 표는 concurrent operation를 구현하기 위해 일반적으로 재정의하는 메서드들이 나와있다.
메서드 | 설명 |
start | (필수) 모든 concurrent operations은 이 메서드를 재정의하고 기본 동작을 고유한 커스텀 구현으로 바꿔야 한다. operation을 수동으로 실행하려면 start 메서드를 호출하면 된다. 따라서 이 메서드의 구현은 operation의 시작점이며 태스크를 실행할 스레드 또는 기타 실행 환경을 설정하는 곳이다. 구현할 때 super를 절대 호출해서는 안된다. |
main | (선택 사항) 이 메서드는 일반적으로 operation object와 관련된 태스크를 구현하는 데 사용된다. start 메서드에서 태스크를 수행 할 수 있지만 이 메서드를 사용하여 태스크를 구현하면 설정 및 태스크 코드가 더 명확하게 분리 될 수 있다. |
isExecuting isFinished |
(필수) Concurrent operation은 실행 환경을 설정하고 해당 환경의 상태를 외부 클라이언트에 보고 할 책임이 있다. 따라서 Concurrent operation은 태스크를 실행하는 시기와 태스크를 완료 한 시기를 알기 위해 일부 상태 정보를 유지해야 한다. 그런 다음 이 메서드를 사용하여 해당 상태를 보고해야 한다. 이러한 메서드의 구현은 다른 스레드에서 동시에 호출해도 안전해야 한다. 또한 이러한 메소드에서 보고하는 값을 변경할 때 예상되는 키 경로에 대해 적절한 KVO notification을 생성해야 한다. |
isConcurrent | (필수) operation을 Concurrent operation으로 식별하려면 이 메서드를 재정의하고 YES를 반환하라 |
이 섹션의 나머지 부분에서는 concurrent operation을 구현하는 데 필요한 기본 코드를 보여주는 MyOperation 클래스의 샘플 구현을 보여준다. MyOperation 클래스는 생성하는 별도의 스레드에서 자체적인 메인 메서드를 실행한다. 메인 메서드이 수행하는 실제 작업은 관련이 없다. 샘플의 요점은 concurrent operation을 정의할 때 제공해야 하는 인프라를 보여주는 것이다.
하단의 코드는 인터페이스와 MyOperation 클래스 구현의 일부를 보여준다. MyOperation 클래스에 대한 isConcurrent, isExecuting 및 isFinished 메서드의 구현은 비교적 간단하다. isConcurrent 메소드는 concurrent operation임을 나타내기 위해 YES를 반환해야 한다. isExecuting 및 isFinished 메서드는 단순히 클래스 자체의 인스턴스 변수에 저장된 값을 반환한다.
@interface MyOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (void)completeOperation;
@end
@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
@end
하단의 코드는 MyOperation의 start 메서드를 보여준다. 이 메서드의 구현은 반드시 수행해야 하는 작업을 최소한으로 보여준다. 이 경우 메서드는 단순히 새 스레드를 시작하고 메인 메서드를 호출하도록 구성한다. 또한 이 메서드는 실행중인 멤버 변수를 업데이트하고 해당 값의 변경 사항을 반영하기 위해 isExecuting 키 경로에 대한 KVO notification을 생성한다. 작업이 완료되면 이 메서드는 단순히 반환되어 새로 분리 된 스레드가 실제 작업을 수행하도록 한다.
- (void)start {
// Always check for cancellation before launching the task.
if ([self isCancelled])
{
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
하단의 코드는 MyOperation 클래스에 대한 나머지 구현을 보여준다. 위의 코드에서 볼 수 있듯이 메인 메서드는 새 스레드의 진입점이다. operation object와 관련된 작업을 수행하고 해당 작업이 최종적으로 완료되면 custom completeOperation 메서드를 호출한다. 그런 다음 completeOperation 메소드는 operation의 상태 변경을 반영하기 위해 isExecuting 및 isFinished 키 경로 모두에 필요한 KVO notifications을 생성한다.
- (void)main {
@try {
// Do the main work of the operation here.
[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
operation이 취소되더라도 KVO observers에게 operation이 완료되었다고 알려야 한다. operation object가 다른 operation object의 완료에 의존하는 경우 해당 오브젝트에 대한 isFinished 키 경로를 모니터링한다. 모든 오브젝트가 완료되었다고 보고 할 때만 dependent operation이 실행할 준비가 되었다는 신호를 보낸다. 따라서 완료 알림을 생성하지 못하면 애플리케이션에서 다른 operation이 실행되지 않을 수 있다.
Maintaining KVO Compliance
NSOperation 클래스는 다음 키 경로에 대해 KVO(key-value observing)를 준수한다.
- isCancelled
- isConcurrent
- isExecuting
- isFinished
- isReady
- dependencies
- queuePriority
- completionBlock
start 메서드를 재정의하거나 main 재정의 이외의 NSOperation 오브젝트의 의미있는 커스터마이징을 했을 경우, custom object가 이러한 키 경로에 대해 KVO를 준수하는지 확인해야 한다. start 메서드를 재정의 할 때 가장 관심을 가져야하는 주요 경로는 isExecuting 및 isFinished이다. 이것들은 해당 메서드를 다시 구현할 때 가장 일반적으로 영향을 받는 주요 경로이다.
다른 operation object 외에 다른 항목에 대한 종속성에 대한 지원을 구현하려는 경우 isReady 메서드를 재정의하고 커스텀 종속성이 충족 될 때까지 강제로 NO를 반환하도록 할 수도 있다. (커스텀 종속성을 구현할 때 NSOperation 클래스에서 제공하는 디폴트 종속성 관리 시스템을 계속 지원하는 경우 isReady 메서드에서 super를 호출해야 한다.) operation object의 준비 상태가 변경되면 isReady 키 경로에 대한 KVO notification을 생성하여 변경 사항을 보고한다. addDependency 또는 removeDependency 메서드를 재정의하지 않는 한 종속성 키 경로에 대한 KVO notification을 생성할 필요가 없다.
NSOperation의 다른 주요 경로에 대해 KVO notification을 생성 할 수 있지만, 그렇게 할 필요가 없을 수도 있다. operation을 취소해야 하는 경우 기존 cancel 메서드를 호출하기만 하면 된다. 마찬가지로 operation object에서 큐의 우선 순위에 대한 정보를 수정할 필요가 거의 없다. 마지막으로 operation이 동시성 상태를 동적으로 변경할 수 없는 경우 isConcurrent 키 경로에 대한 KVO notification을 제공 할 필요가 없다.
Customizing the Execution Behavior of an Operation Object
operation object의 설정은 생성한 후 큐에 추가하기 전에 발생한다. 이 섹션에서 설명하는 설정 유형은 NSOperation을 직접 상속받아 사용했는지 또는 기존 서브 클래스를 사용했는지에 관계없이 모든 operation object에 적용 할 수 있습니다.
Configuring Interoperation Dependencies
종속성은 고유한 operation object의 실행을 직렬화하는 방법이다. 다른 operation에 종속 된 operation은 종속 된 모든 operation의 실행이 완료 될 때까지 실행을 시작할 수 없다. 따라서 종속성을 사용하여 두 operation object 간에 간단한 일대일 종속성을 만들거나 복잡한 오브젝트 종속성 그래프를 작성할 수 있다.
두 operation object 간의 종속성을 설정하려면 NSOperation의 addDependency 메서드를 사용하면 된다. 이 메서드는 현재 operation object에서 매개 변수로 지정한 타겟 operation에 대한 단방향 종속성을 만들 수 있다. 이 종속성은 타겟 오브젝트가 실행을 완료 할 때까지 현재 오브젝트가 실행을 시작할 수 없음을 의미한다. 또한 종속성은 operation 끼리 동일한 큐가 제한되지 않는다. operation object는 자체 종속성을 관리하므로 operation 간에 종속성을 생성하고 모든 종속성을 서로 다른 큐에 추가할 수 있다. 그러나 허용되지 않는 한 가지는 작업간에 순환 종속성(circular dependencies)을 만드는 것이다. 그렇게하면 영향을 받는 operation이 실행되지 않도록 하는 프로그래머 오류이다.
operation의 모든 종속성이 자체적으로 실행을 마치면 operation object는 일반적으로 실행할 준비가 된다. (isReady 메서드의 동작을 커스텀 하는 경우 설정한 기준에 따라 operation 준비가 결정된다.) operation object가 큐에 있으면 큐는 언제든지 해당 operation을 실행할 수 있다. operation을 수동으로 실행하려는 경우 operation의 start 메서드를 호출하는 것은 사용자에게 달려 있다.
중요 : operation을 실행하거나 operation queue에 추가하기 전에 항상 종속성을 구성해야 한다. 종속성을 나중에 추가하면 지정된 operation object가 실행되지 않을 수 있다.
종속성은 오브젝트의 상태가 변경 될 때마다 적절한 KVO notification을 보내는 것은 각 operation object에 의존한다.
operation object의 동작을 커스터마이징 하는 경우 종속성 문제를 방지하기 위해 커스텀 코드에서 적절한 KVO notification을 생성해야 할 수 있다.
Changing an Operation’s Execution Priority
큐에 추가 된 operation의 경우 먼저 대기 중인 operation의 준비 상태에 따라 실행 순서가 결정된 다음 상대적 우선 순위에 따라 결정된다. 준비 상태는 다른 operation에 대한 operation의 종속성에 의해 결정되지만 우선 순위 수준은 operation object 자체의 속성이다. 기본적으로 모든 새 operation object는 "normal" 우선 순위를 갖지만 오브젝트의 setQueuePriority 메소드를 호출하여 필요에 따라 우선 순위를 높이거나 낮출 수 있다.
우선 순위 수준은 동일한 큐에 있는 operation에만 적용된다. 애플리케이션에 여러 operation queue가 있는 경우 각각의 큐는 다른 큐와 독립적으로 operation의 우선 순위를 지정한다. 따라서 우선 순위가 낮은 operation이 다른 큐에서 우선 순위가 높은 operation이 실행 되기 전에 실행할 수 있다.
우선 순위 수준은 종속성을 대체하지 않는다. 우선 순위는 operation queue에 현재 준비된 operation 중에서 실행하는 시작 순서를 결정한다. 예를 들어 큐에 우선 순위가 높은 operation과 우선 순위가 낮은 operation이 모두 포함되어 있고 두 operation이 모두 준비된 경우 큐는 우선 순위가 높은 operation을 먼저 실행한다. 그러나 우선 순위가 높은 operation이 준비되지 않고 우선 순위가 낮은 operation이 준비된 경우 큐는 우선 순위가 낮은 operation을 먼저 실행한다. 하나의 operation이 다른 operation이 완료 될 때까지 시작되지 않도록 하려면 우선순위 대신 종속성을 사용해야 한다.
Changing the Underlying Thread Priority
OS X v10.6 이상에서는 operation의 기본 스레드의 실행 우선 순위를 설정 할 수 있다. 시스템의 스레드 정책은 자체적으로 커널에 의해 관리되지만 일반적으로 우선 순위가 높은 스레드는 우선 순위가 낮은 스레드보다 실행 기회가 더 많다. operation object에서 스레드 우선 순위를 0.0에서 1.0 사이의 부동 소수점 값으로 지정할 수 있다. 0.0은 가장 낮은 우선 순위이고 1.0은 가장 높은 우선 순위이다. 명시적으로 스레드 우선 순위를 지정하지 않으면 operation은 기본 스레드에서 우선 순위 0.5로 실행된다.
operation의 스레드 우선 순위를 설정하려면 operation object를 큐에 추가하기 전에 (또는 수동으로 실행하기 전에) setThreadPriority 메소드를 호출해야 한다. operation을 실행할 때가 되면 디폴트 start 메서드는 지정한 값을 사용하여 현재 스레드의 우선 순위를 수정한다. 이 새로운 우선 순위는 operation의 메인 메서드에서만 유효하다. 다른 모든 코드 (operation의 completion block 포함)는 디폴트 스레드 우선 순위로 실행된다. concurrent operation을 생성하여 start 메서드를 재정의하는 경우 스레드 우선 순위를 직접 구성해야 한다.
Setting Up a Completion Block
OS X v10.6 이상에서 operation은 메인 태스크 실행이 완료되면 completion block을 실행할 수 있다. completion block을 사용하여 메인 태스크의 일부로 간주하지 않는 작업을 수행 할 수 있다. 예를 들어 이 블록을 사용하여 operation object가 완료되었음을 관심있는 클라이언트에 알릴 수 있다. concurrent operation object는 이 블록을 사용하여 final KVO notification을 생성 할 수 있다.
completion block을 설정하려면 NSOperation의 setCompletionBlock 메서드를 사용하면 된다. 이 메서드에 전달하는 블록에는 인수와 반환 값이 없어야 한다.
Tips for Implementing Operation Objects
Operation Object는 구현하기 매우 쉽지만 코드를 작성할 때 알아야 할 몇 가지 사항이 있다. 다음 섹션에서는 Operation Object에 대한 코드를 작성할 때 고려해야 할 요소에 대해 설명한다.
Managing Memory in Operation Objects
다음 섹션에서는 Operation Object에서 좋은 메모리 관리의 핵심 요소를 설명한다.
Avoid Per-Thread Storage
대부분의 operation은 스레드에서 실행되지만 nonconcurrent operation의 경우 해당 스레드는 일반적으로 Operation Queue에 의해 제공된다. Operation Queue가 스레드를 제공하는 경우 해당 스레드가 operation에 의해 처리되지 않고 큐가 소유하는 것으로 간주해야 한다. 특히, 직접 생성하거나 관리하지 않는 스레드와 데이터를 연결해서는 안 된다. Operation Queue에서 관리하는 스레드는 시스템 및 애플리케이션의 요구 사항에 따라 왔다 갔다한다. 따라서 스레드 별 저장소를 사용하는 operation 간에 데이터를 전달하는 것은 신뢰할 수 없으며 실패 할 가능성이 높다.
operation object의 경우 어떤 경우에도 스레드 별 저장소를 사용할 이유가 없어야 한다. operation object를 초기화 할 때 operation을 수행하는 데 필요한 모든 것을 오브젝트에 제공해야한다. 따라서 operation object 자체가 필요한 컨텍스트 저장소를 제공한다. 모든 수신 및 발신 데이터는 애플리케이션에 다시 통합되거나 더 이상 필요하지 않을 때까지 컨텍스트 저장소에 저장해야 한다.
Keep References to Your Operation Object As Needed
operation object가 비동기적으로 실행된다고 오브젝트를 생성하고 잊어버릴 수 있다고 가정해서는 안 된다. 그것들은 여전히 오브젝트일 뿐이며 코드에 필요한 모든 참조를 관리하는 것은 사용자의 몫이다. operation이 완료된 후 operation에서 결과 데이터를 검색해야 하는 경우 특히 중요하다.
operation에 대한 자체 참조를 항상 유지해야 하는 이유는 나중에 오브젝트에 대해 큐에게 물어 볼 기회가 없을 수 있기 때문이다. 큐는 가능한 한 빨리 operation을 dispatch하고 실행하기 위해 모든 노력을 기울인다. 대부분의 경우 큐는 추가 된 직후 operation 실행을 시작한다. operation에 대한 참조를 얻기 위해 너의 코드가 큐로 들아갈 때쯤 해당 operation이 이미 완료되어 큐에서 제거 될 수 있기 때문이다.
Handling Errors and Exceptions
operation은 기본적으로 애플리케이션 내부에서 별개의 엔티티이므로 발생하는 오류나 예외를 처리해야 한다. OS X v10.6 이상에서 NSOperation 클래스에서 제공하는 디폴트 start 메서드는 예외를 포착하지 않는다. (OS X v10.5에서 start 메서드는 예외를 catch하고 suppress한다.) 커스텀 코드는 항상 예외를 직접 catch하고 suppress해야 한다. 또한 오류 코드를 확인하고 필요에 따라 애플리케이션의 해당 부분을 알려야 한다. 그리고 start 메서드를 바꾸는 경우에도 마찬가지로 기본 스레드의 범위를 벗어나지 않도록 모든 예외를 catch 해야 한다.
다음과 같은 오류 상황을 처리할 준비가 되어 있어야 한다.
- UNIX errno-style 오류 코드를 확인하고 처리해야 한다.
- 메서드 및 기능에서 반환된 명시적 오류 코드를 확인해야 한다.
- 사용자 코드 또는 다른 시스템 프레임워크에서 발생한 예외를 Catch해야 한다.
- NSOperation 클래스 자체에서 발생하는 예외를 Catch하면, 예외는 다음 상황에서 발생한다.
- operation을 실행할 준비가 되지 않았지만 start 메서드가 호출 된 경우
- operation이 실행 중이거나 완료되고 (취소된 경우일 수 도 있음) start 메서드가 다시 호출 될 때
- 이미 실행 중이거나 완료된 operation에 completion block을 추가하려고 할 때
- 취소된 NSInvocationOperation 오브젝트의 결과를 검색하려고 할 때
커스텀 코드에서 예외 또는 오류가 발생하면 해당 오류를 애플리케이션의 나머지 부분에 알리는 데 필요한 모든 단계를 수행해야 한다.
NSOperation 클래스는 오류 결과 코드 또는 예외를 애플리케이션의 다른 부분에 알리기 위한 명시적 메서드를 제공하지 않는다.
따라서 이러한 정보가 애플리케이션에 중요한 경우 필요한 코드를 작성해야 한다.
Determining an Appropriate Scope for Operation Objects
operation queue에 임의로 많은 operation을 추가 할 수 있지만 그렇게 하는 것은 종종 실용적이지 않다. 다른 오브젝트와 마찬가지로 NSOperation 클래스의 인스턴스는 메모리를 소비하고 실행과 관련된 실제 비용이 있다. 적은 양의 작업만 수행하는 operation object 수만 개를 생성하는 경우 실제 작업을 수행하는 것보다 operation을 dispatch 하는 데 더 많은 시간을 소비하고 있음을 알 수 있다. 또한 이미 메모리가 제한된 애플리케이션의 경우 메모리에 수만 개의 operation object만 있으면 성능이 더욱 저하 될 수 있다.
operation을 효율적으로 사용하기 위한 열쇠는 수행해야 하는 작업의 양과 컴퓨터를 계속 바쁘게 유지하는 것 사이에서 적절한 균형을 찾는 것이다. operation이 합당한 양의 작업을 수행하는지 확인해라. 예를 들어 애플리케이션이 100개의 서로 다른 값에 대해 동일한 태스크를 수행하기 위해 100개의 operation object를 만드는 경우 10개의 operation object를 만들어 각각 10개의 값을 처리하는 것이 좋다.
또한 한 번에 많은 수의 operation을 큐에 추가하거나 operation object를 처리할 수 있는 속도보다 빠르게 지속적으로 추가하지 않도록 해야한다. operation object로 큐를 가득 채우지 않고 해당 오브젝트를 일괄적으로 생성하라. 한 배치에서 실행이 완료되면 completion block을 사용하여 애플리케이션에 새 배치를 생성하도록 지시해라. 수행 할 작업이 많은 경우 큐를 충분한 operation으로 채워서 컴퓨터 상태를 바쁘게 유지하려고 하지만, 한 번에 너무 많은 operation을 만들어 애플리케이션의 메모리가 부족 해지는 것은 하지 말아야 한다.
물론 사용자가 생성하는 operation object의 수와 각각에서 수행하는 작업의 양은 가변적이며 전적으로 애플리케이션에 따라 다르다. 효율성과 속도 사이에서 적절한 균형을 찾는 데 도움이 되는 Instruments와 같은 도구를 항상 사용해야 한다.
Executing Operations
궁극적으로 애플리케이션은 관련 작업을 수행하기 위해 Operation을 실행해야 한다. 이 섹션에서는 Operation을 실행하는 여러 방법과 런타임에 Operation 실행을 조작하는 방법에 대해 알아본다.
Adding Operations to an Operation Queue
Operation을 실행하는 가장 쉬운 방법은 NSOperationQueue 클래스의 인스턴스인 Operation Queue를 사용하는 것이다. 애플리케이션은 사용하려는 모든 Operation Queue를 만들고 유지 관리 할 책임이 있다. 애플리케이션에는 큐의 수에 제한이 없지만 주어진 시점에서 실행할 수 있는 Operation 수에는 실질적인 제한이 있다. Operation Queue는 시스템과 함께 작동하여 concurrent operations의 수를 사용 가능한 코어 및 시스템 로드에 적합한 값으로 제한한다. 따라서 추가 큐를 생성한다고해서 추가 Operation을 실행할 수 있는 것은 아니다.
큐를 만드려면 다른 오브젝트와 마찬가지로 애플리케이션에서 큐를 할당한다.
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
큐에 operation을 추가하려면 addOperation 메서드를 사용한다. OS X v10.6 이상에서는 addOperations:waitUntilFinished: 메서드를 사용하여 operation 그룹을 추가하거나 addOperationWithBlock 메서드를 사용하여 (해당 operation object 없이) 큐에 블록 오브젝트를 직접 추가 할 수 있다. 이러한 각 메서드는 operation(또는 operations)을 큐에 추가하고 처리를 시작해야 함을 큐에 알린다. 대부분의 경우 operation은 큐에 추가 된 직후에 실행되지만 operation queue는 여러 가지 이유로 큐에 있는 operation의 실행을 지연시킬 수 있다. 특히 대기 중인 operation이 아직 완료되지 않은 다른 operation에 종속 된 경우 실행이 지연될 수 있다. operation queue 자체가 일시 중단되었거나 이미 실행중인 operation의 개수가 최대 concurrent operation 수 일 경우에도 실행이 지연 될 수 있다.
다음 예는 operation을 큐에 추가하기 위한 기본 구문을 보여준다.
[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
[aQueue addOperationWithBlock:^{
/* Do something. */
}];
중요 : Operation Object를 큐에 추가하기 전에 필요한 모든 설정 및 수정을 수행해야 한다. 추가 된 Operation은 언제든지 실행될 수 있으며 변경 사항이 의도한 효과를 갖기에는 너무 늦을 수 있기 때문이다.
NSOperationQueue 클래스는 operation의 동시 실행을 위해 설계되었지만 싱글 큐에서 한 번에 하나의 operation만 실행하도록 강제 할 수 있다. setMaxConcurrentOperationCount 메서드를 사용하면 operation queue 오브젝트에 대한 최대 concurrent operation 수를 설정 할 수 있다. 이 메서드에 값을 1로 전달하면 큐가 한 번에 하나의 operation만 실행한다. 한 번에 하나의 operation만 실행할 수 있지만 실행 순서는 여전히 각 operation의 준비 상태 및 할당된 우선 순위와 같은 다른 요소를 기반으로 한다. 따라서 serialized operation queue는 Grand Central Dispatch의 serial dispatch queue과 동일한 동작을 제공하지 않는다. operation object의 실행 순서가 중요한 경우 operation을 큐에 추가하기 전에 종속성을 사용하여 해당 순서를 설정해야 한다.
Executing Operations Manually
operation queue은 operation object를 실행하는 가장 편리한 방법이지만 큐를 사용하지 않고 작업을 실행할 수도 있다. 그러나 수동으로 operation을 실행하도록 선택하는 경우 코드에서 몇 가지 예방 조치를 취해야한다. 특히 operation을 실행할 준비가 되어 있어야 하며 항상 start 메서드를 사용하여 시작해야 한다.
isReady 메소드가 YES를 리턴 할 때까지 조작을 실행할 수 없는 것으로 간주된다. isReady 메서드는 NSOperation 클래스의 종속성 관리 시스템에 통합되어 operation의 종속성 상태를 제공한다. 종속성이 지워질 때만 operation을 실행할 수 있다.
Operation을 수동으로 실행할 때는 항상 start 메서드를 사용하여 실행을 시작해야 한다. start 메서드는 실제로 커스텀 코드를 실행하기 전에 몇 가지 안전 검사를 수행하기 때문에 main 또는 다른 메서드 대신 이 메서드를 사용한다. 특히 디폴트 start 메서드는 operation가 종속성을 올바르게 처리하는 데 필요한 KVO notification을 생성합니다. 또한 이 메서드는 이미 operation이 취소된 경우 올바르게 실행되는 것을 방지하고 실제로operation이 실행할 준비가 되지 않은 경우 예외를 throw한다.
애플리케이션에서 concurrent operation object를 정의하는 경우 operation을 시작하기 전에 isConcurrent 메서드 호출도 고려해야 한다. 이 메서드가 NO를 반환하는 경우 로컬 코드가 현재 스레드에서 operation을 동기적으로 실행할지 아니면 먼저 별도의 스레드를 만들지 결정할 수 있다. 그러나 이러한 종류의 검사를 구현하는 것은 전적으로 당신에게 달려 있다.
하단의 코드는 operation을 수동으로 실행하기 전에 수행해야 하는 검사 유형의 간단한 예를 보여준다. 메서드가 NO를 반환하면 타이머를 예약하고 나중에 메서드를 다시 호출 할 수 있다. 그런 다음 메서드가 YES를 반환 할 때까지 타이머를 계속 다시 예약한다. 이는 작업이 취소되었기 때문에 발생할 수 있다.
- (BOOL)performOperation:(NSOperation*)anOp
{
BOOL ranIt = NO;
if ([anOp isReady] && ![anOp isCancelled])
{
if (![anOp isConcurrent])
[anOp start];
else
[NSThread detachNewThreadSelector:@selector(start)
toTarget:anOp withObject:nil];
ranIt = YES;
}
else if ([anOp isCancelled])
{
// If it was canceled before it was started,
// move the operation to the finished state.
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
// Set ranIt to YES to prevent the operation from
// being passed to this method again in the future.
ranIt = YES;
}
return ranIt;
}
Canceling Operations
operation queue에 추가되면 operation object는 큐에 의해 효과적으로 소유되므로 제거 할 수 없다. operation을 큐에서 빼는 유일한 방법은 operation을 취소하는 것이다. cancel 메서드를 호출하여 single individual operation object를 취소하거나 큐 오브젝트의 cancelAllOperations 메서드를 호출하여 큐의 모든 operation object를 취소 할 수 있다.
더 이상 필요하지 않다고 확신하는 경우에만 operation을 취소해야 한다. 취소 명령을 실행하면 operation object가 "canceled" 상태가 되어 실행되지 않는다. 취소 된 operation은 여전히 "finished" 된 것으로 간주되기 때문에 해당 operation에 종속 된 개체는 해당 종속성을 지우는 적절한 KVO notification을 받는다. 따라서 operation을 선택적으로 취소하는 것보다 애플리케이션 종료 또는 사용자가 구체적으로 취소를 요청하는 것과 같은 중요한 이벤트에 대한 응답으로 대기중인 모든 operation을 취소하는 것이 더 일반적이다.
Waiting for Operations to Finish
최상의 성능을 얻으려면 operation을 가능한 한 비동기적으로 설계하여 operation이 실행되는 동안 애플리케이션에서 추가 작업을 수행 할 수 있도록해야 한다. operation을 생성하는 코드가 해당 operation의 결과도 처리하는 경우 NSOperation의 waitUntilFinished 메서드를 사용하여 operation이 완료 될 때까지 해당 코드를 차단할 수 있다. 그러나 일반적으로 가능하다면 이 메서드를 호출하지 않는 것이 가장 좋다. 현재 스레드를 차단하는 것이 편리한 솔루션일 수 있지만, 이 솔루션은 코드에 더 많은 직렬화를 도입하고 전체적인 동시성의 양을 제한한다.
중요 : 애플리케이션의 메인 스레드에서 operation을 기다리면 안 된다. 보조 스레드 또는 다른 operation에서만 기다려야 한다. 메인 스레드를 차단하면 애플리케이션이 사용자 이벤트에 응답하지 못하며 애플리케이션이 응답하지 않는 것처럼 보일 수 있다.
single operation이 완료 될 때까지 기다리는 것 외에도 NSOperationQueue의 waitUntilAllOperationsAreFinished 메서드를 호출하여 큐의 모든 operation을 기다릴 수도 있다. 모든 큐가 완료 될 때까지 기다릴 때 애플리케이션의 다른 스레드는 여전히 큐에 operation을 추가하여 대기 시간을 연장 할 수 있다.
Suspending and Resuming Queues
operation 실행을 일시적으로 중지하려는 경우 setSuspended 메서드를 사용하여 해당 operation queue를 일시 중지 할 수 있다. 큐를 일시 중지해도 이미 실행중인 operation이 태스크 중간에 일시 중지되는 것은 아니다. 단순히 새로운 operation이 실행되도록 스케줄링 되는 것을 방지한다. 진행중인 작업을 일시 중지하라는 사용자 요청에 대한 응답으로 큐를 일시 중지 할 수 있다. 사용자가 결국 해당 작업을 다시 시작하기를 원할 수 있기 때문이다.
[원문]
Operation Queues
Operation Queues Cocoa operations are an object-oriented way to encapsulate work that you want to perform asynchronously. Operations are designed to be used either in conjunction with an operation queue or by themselves. Because they are Objective-C based,
developer.apple.com
'프로그래밍 > iOS' 카테고리의 다른 글
Concurrency Programming Guide (5) - Dispatch Sources (0) | 2020.12.26 |
---|---|
Concurrency Programming Guide (4) - Dispatch Queues (0) | 2020.12.25 |
Concurrency Programming Guide (2) - Concurrency and Application Design (0) | 2020.12.23 |
Concurrency Programming Guide (1) - 소개 (0) | 2020.12.23 |
Threading Programming Guide (2) - About Threaded Programming (0) | 2020.12.21 |