티스토리 뷰

반응형

이번 글은 Apple에서 과거에 올린 글이기 때문에 Objective-C로 코드가 구성되어있는데 Swift로 구현하는 방법은 여기 참고해주세요!.

Apple Developer Document - Operation Queues

Operation Queues

Cocoa Operation은 비동기적으로 실행하려는 작업을 객체 지향 방식으로 캡슐화한다. 작업들은 operation queue와 함께 실행되거나 자체적으로 실행되도록 설계된다. OS X, iOS의 Cocoa 기반 프로그램들에서 일반적으로 사용되는 작업들은 Objective-C 기반이다. 이번 글에서는 Operation을 정의하고 사용하는 방법에 대해 알아보자

About Operation Object

작업 객체는 NSOperation 클래스의 인스턴스로 프로그램에서 사용될 작업을 캡슐화하는 데 사용된다. NSOperation 클래스는 그 자체는 추상 클래스이기 때문에 이를 상속받는 서브클래스를 만들어야 유용하게 사용할 수 있다. 추상 클래스이지만 이 클래스는 서브 클래스에서 수행해야 하는 작업의 양을 최소화하기 위해 많은 것을 제공한다. 또한 Foundation 프레임워크는 NSOperation의 구체적인 두 가지 서브클래스를 제공하여 쉽게 사용할 수 있도록 도와준다. 아래 표에는 이러한 클래스와 각 클래스의 사용법이 요약되어있다.

Class Description
NSInvocationOperation 클래스를 그대로 사용하게 되면 프로그램에서 object, selector를 기반으로 작업 객체를 만드는 것이다. 필요한 task를 이미 수행하는 기존 메서드가 있는 경우 이 클래스를 사용할 수 있다. 서브클래싱이 필요하지 않기때문에 이 클래스를 사용하여 보다 동적으로 작업 객체를 만들 수 있다.
NSBlockOperation 클래스를 그대로 사용하게 되면 하나 이상의 블록 객체를 실행한다. 하나 이상의 블록을 실행할 수 있기 때문에 작업 블록 객체는 group semantic을 사용하여 작동한다. 그룹화 되어있는 모든 블록의 실행이 완료된 경우에만 작업이 완료된 것으로 간주된다.
NSOperation 사용자 정의 작업 객체를 정의하는 base 클래스이다. NSOperation을 상속하게 되면 작업을 완전히 제어할 수 있게되는것 뿐만아니라 작업이 실행되고 상태를 보고하는 기능도 제공한다.

모든 작업 객체는 다음과 같은 주요 기능을 지원한다.

 

  • 작업 객체간의 그래프 기반 종속성의 설정을 지원한다. 이러한 종속성은 종속된 모든 작업이 실행을 마칠 때까지 지정된 작업이 실행되지 않도록 해준다.
  • 작업의 주요 작업이 완료된 후에 실행되는 블록을 설정하는 방법을 지원한다.
  • 실행 상태의 변화를 KVO 알림을 통해서 모니터링한다.
  • 작업의 우선순위를 정해서 실행 순서에 영향을 준다.
  • 실행 중인 작업을 중지할 수 있는 cancelling semantic을 지원한다.

작업은 프로그램의 동시성 수준을 향상시키는데 도움이 되기 위해 설계되었고 프로그램의 동작을 개별적으로 구성하고 캡슐화하는 좋은 방법이다. 프로그램의 메인 스레드에서 약간의 코드를 실행하는 대신 queue에 하나 이상의 작업 객체를 추가하고 하나 이상의 별도 스레드에서 작업을 비동기식으로 수행할 수 있다.

Concurrent Versus Non-concurrent Operations

Operation queue에 작업들을 추가하여 작업들을 실행하지만 반드시 그렇게 할 필요는 없다. start 메서드를 호출하여 작업 객체를 수동으로 실행할 수도 있지만 동시성을 고려했을 때엔 이점이 없다. NSOperaion 클래스의 isConcurrent 메서드는 작업이 동기식으로 진행 중인지 비동기식으로 진행 중인지 알려준다. 기본적으로 이 메서드는 NO를 리턴하는데 이는 메서드를 호출한 스레드에서 작업들이 동기적으로 실행되고 있음을 의미한다.

 

만약 작업들을 동시에 실행, 즉 비동기적으로 구현하고 싶다면 코드를 추가적으로 작성하여 작업을 비동기적으로 시작해야한다. 예를 들어 별도의 스레드를 생성하거나 비동기 시스템 함수를 호출하거나 start 메서드가 task를 시작하고 완료되기 전에 리턴하는 방법으로 작업을 비동기적으로 시작할 수 있다.

 

작업을 queue에 추가하여 실행하는 방법을 사용하면 개발자들은 concurrent operation object를 따로 구현할 필요는 없다. 만약 non-concurrent operation을 operating queue에 추가하게 되면 큐는 자체적으로 해당 작업을 실행하기 위한 스레드를 만들어 계속해서 동시에 실행한다. 따라서 동시성 작업을 정의하는 기능은 작업을 queue로 실행하지 않고 수동으로 실행하지만 비동기식으로 진행하고 싶을 때만 구현해주면 된다.

Creating an NSInvocationOperation Object

NSOperation의 구체적인 서브 클래스인 NSInvocationOperation 클래스는 실행될 때 지정한 object에서 지정한 selector를 호출한다. 이 클래스를 사용하여 프로그램의 task에 대해 사용자 정의 작업 객체를 정의하는 것은 피하는 것이 좋다. 실행 중인 프로그램을 수정하고 있고 실행할 task에 필요한 객체와 메서드가 이미 있는 경우에 사용해야 한다. 호출하려는 방법이 상황에 따라 변경될 수 있는 경우에도 사용할 수 있다. 예를 들어 사용자의 입력에 따라 선택되는 것이 동적으로 변하는 경우 NSInvocationOperation 작업을 사용할 수 있다.

 

Invocation Operation을 만드는 과정은 간단하다. object, selector를 정하여 새로운 인스턴스를 생성하면 된다. 다음 코드는 custom 클래스에서 process를 만드는데 사용한 두 가지 메서드를 보여준다. taskWithData: 메서드는 새로운 invocation 객체를 만들고 task 구현을 포함하는 다른 메서드의 이름을 매개변수로 받는다.

@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

NSOperation의 구체적인 서브 클래스인 NSBlockOperation 클래스는 하나 이상의 블록 객체에 대해 wrapper 역할을 수행한다. 이 클래스는 이미 operation queue를 사용하고 있지만 dispatch queue를 생성하지 않으려는 프로그램에 객체 지향 wrapper를 제공한다. 블록 작업을 사용하여 작업의 종속성, KVO 알림 및 dispatch queue에서는 사용할 수 없는 기타 기능들을 활용할 수 있다.

 

블록 작업을 만들때는 일반적으로 하나 이상의 블록을 추가한다. 물론 나중에 더 추가할 수도 있다. NSBlockOperation을 실행할 때가 되면 객체는 모든 블록을 default 우선순위로 concurrent disptch queue에 추가한다. 그런 뒤 모든 블록이 실행 완료되는 것을 기다린다. 마지막 블록이 실행 완료되면 작업 객체는 자체적으로 완료했다고 기록한다. thread join을 사용하여 여러 개의 스레드의 결과를 병합하는 것처럼 실행되고 있는 블록 그룹을 추적할 수도 있다. 차이점은 블록 작업 자체는 각각 별도의 스레드에서 실행되기 때문에 프로그램의 다른 스레드는 블록 작업이 완료될 때까지 다른 작업을 계속 수행할 수 있는 것이다. 다음 코드는 NSBlockOperation 인스턴스를 생성하는 간단한 예를 보여준다. 블록 자체에는 매개 변수가 없고 return값도 없다.

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
      NSLog(@"Beginning operation.\n");
      // Do some work.
   }];

이렇게 block operation 객체를 만들고 후에 블록을 추가하고 싶다면 addExecutionBlock 메서드를 사용하면 된다. 만약 블록들이 순차적으로 실행되게 하고싶다면 원하는 dispatch queue에 직접 추가해야 한다.

Defining a Custom Operation Object

block operation, invocation operation 객체가 프로그램의 요구 사항을 완전히 충족시키지 못한다면 NSOperation을 직접 상속한 뒤 필요한 동작을 추가할 수 있다. NSOperation 클래스는 모든 작업 객체에 대한 일반적인 서브 클래싱 지점을 제공한다. 또한 이 클래스는 종속성 및 KVO 알림에 필요한 대부분의 작업을 처리하기 위해 많은 인프라를 제공한다. 이러한 인프라도 작업이 올바르게 작동하도록 하기 위해 보완해줘야 하는 경우도 있다. 추가해야 하는 것들은 프로그램이 동시성, 비동시 성인지에 따라 다르다.

 

비동시성 작업을 정의하는 것은 당연하게도 동시성 작업보다는 간단하다. 비동시 작업의 경우 하나의 동작만 만들어주면 되는데, 실행 중인 작업에 대해 수행, 취소 이벤트만 적절하게 응답하도록 하면 된다. 동시성 작업의 경우 기존 인프라 중 일부를 사용자가 직접 바꿔줘야 한다. 다음 섹션에서는 두 유형의 객체를 구현하는 방법에 대해 알아보자.

Performing the Main Task

모든 작업 객체는 최소한 다음의 메서드는 가지고 있어야 한다.

 

  • A custom initialization method (생성자)
  • main

작업 객체를 원하는 대로 만들기 위해 생성자가 필요하며 작업 객체를 실행하기 위해 main 메서드가 필요하다. 물론 다음과 같은 추가 메서드도 구현할 수 있다.

 

  • main 메서드가 실행될 때 호출할 custom methods
  • 데이터와 작업의 결과에 접근할 수 있는 Accessor methods
  • NSCoding 프로토콜을 채택한 메서드로 작업 객체를 보관, 삭제할 수 있다.

다음 코드는 NSOperation을 상속한 서브클래스의 시작 템플릿이다. 여기선 취소 처리 방법을 추가하지는 않았다. 이는 잠시 후에 알아보도록 하자. 이 클래스의 초기화 방법은 데이터를 매개변수로 받아 이를 작업 객체 내부에 저장한다.

@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

작업은 작업이 실행을 시작한 후 작업이 끝날 때까지 혹은 누군가 작업을 취소할 때까지 계속 작업을 수행한다. 작업은 시작되기 전에 취소할 수도 있다. NSOperation 클래스는 클라이언트가 작업을 취소할 수 있는 방법을 제공하지만 취소 이벤트를 인식하는 것을 선택이다. 하지만 취소 이벤트를 인식하지 않으면 작업이 완전히 종료된 뒤 할당된 리소스를 회수하지 못할 수도 있다. 따라서 작업 객체는 실행 중 취소된다면 취소 이벤트를 확인한 뒤 종료하는 것이 좋다.

 

작업 객체를 취소하려면 isCancelled 메서드를 주기적으로 호출하여 YES를 반환하면 즉시 종료하면 된다. 작업이 NSOPeration로 직접 만들었건 서브 클래스로 만들었건 상관없이 취소에 대한 지원은 중요하다. isCancelled 메서드 자체는 매우 간단하고 성능 저하 없이 자주 호출할 수 있다. 하지만 아무데서나 호출하긴 좀 그렇기 때문에 다음과 같은 곳에서 호출하는 것을 고려하자.

 

  • 실제 작업을 수행하기 전에
  • 반복문에서 반복이 많을 경우 반복마다 한 번씩
  • 작업 중단이 쉬운 어디서나

다음 코드는 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.
   }
}

위의 코드에는 cleanup 코드가 없지만 실제로 사용할 땐 사용해줘야 한다.

 

Configuring Operations for Concurrent Execution

작업 객체는 기본적으로 동기식으로 실행된다. 즉 start 메서드를 호출하는 스레드에서 작업을 실행한다. 하지만 Operation queue는 비동시 작업에 스레드를 제공하기 때문에 대부분의 작업은 비동기적으로 실행된다. 하지만 비동기식 작업을 수동으로 실행하려는 경우에 적절히 코딩을 해줘서 작업을 수행해야 한다. 즉 작업 객체를 동시 작업으로 선언해야 한다.

 

다음은 동시성 작업에 override 하여 사용하는 메서드들이다.

Method Description
start (Required) 모든 동시 작업은 이 메서드를 override하여 기본 동작을 원하는 대로 바꿔야한다. 작업을 수동으로 실행하려면 start메서드를 호출하면 되는데 start 메서드는 작업의 시작점에서 호출되며 작업을 실행할 스레드, 기타 실행 환경을 설정해주면 된다. 절대로 super로 슈퍼클래스의 메서드를 사용하면 안된다.
main (Optional) 작업 객체와 관련된 작업을 구현하는데 사용된다. start 메서드에서 task를 수행 할 수도 있지만 main 메서드를 사용하여 task를 구현하면 설정과 task 코드가 명확하게 분리될 수 있다.
isExecuting
isFinished
(Required) 동시성 작업은 실행 환경을 설정하고 해당 환경의 상태를 외부 클라이언트에 보고해야한다. 따라서 언제 작업을 실행하고 완료했는지에 대한 여부를 저장해둬야한다. 이 메서드의 구현은 다른 스레드에서 동시에 호출할 수도 있기 때문에 그러한 부분에서 안전성을 보장해야한다. 또한 이 메서드로 보고된 값이 변경될 때 KVO 알림을 생성해줘야한다.
isConcurrent (Required) 작업이 동시에 실행중인지에 대한 여부. 지금은 동시성 작업을 정의하는 방법을 알아보는 중이기 때문에 YES를 반환해주면된다.

아래 코드는 MyOperation 클래스의 구현을 보여주는 예제 코드이다. 이 예제에는 동시성 작업을 구현하는데 필요한 기본 코드를 보여준다. MyOperation 클래스는 자체적으로 만든 스레드에서 main 메서드를 실행한다. 이 코드의 main 메서드가 수행하는 실제 작업은 관련이 없다. 예제의 요점은 동시성 작업을 구현할 때 제공해야 하는 인프라를 보여주는 것이다.

@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 클래스의 인터페이스와 구현의 일부를 보여준다. MyOperation 클래스에 대한 isConcurrent, isExecuting, isFinished 메서드의 구현은 비교적 간단하다. isCurrent 메서드는 단순히 YES만 반환하여 이것이 동시성 작업이라는 것을 표현해주면 되고 isExecuting, isFinished 메서드는 단순히 작업이 진행 중인지 아닌지에 대한 프로퍼티의 값을 반환한다.

 

- (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 클래스의 start 메서드이다. 위 코드는 최소한으로 수행해야 하는 작업을 보여주기 위해 최소화된 것이다. 이 경우 메서드는 단순히 새 스레드를 시작하고 main 메서드를 호출하도록 구성되었다. 또한 실행 멤버 변수들을 업데이트하고 해당 값의 변경을 반영하기 위해 isExecuting 키 경로에 대한 KVO 알림을 생성한다. 작업이 완료되면 이 메서드는 그냥 반환되어 새로 분리된 스레드가 실제 작업을 수행하게 한다.

 

- (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"];
}

위의 코드는 MyOperation 클래스의 나머지 코드이다. 아까 본 main 메서드는 새 스레드의 진입점이다. 작업 객체와 연관된 작업을 수행하고 해당 작업이 끝나게 되면 completeOperation 메서드를 호출한다. 그런 뒤 completeOperation 메서드는 isExecuting, isFinished 키 경로에 모두 필요한 KVO 알림을 생성하여 작업 상태가 변경된 것을 반영한다.

 

만약 작업이 취소되더라도 항상 작업이 끝났다는 것을 KVO 옵저버에게 알려야 한다. 작업 객체가 다른 작업에 종속되었다면 종속된 모든 작업이 끝나야 해당 작업이 실행될 수 있기 때문에 종속된 작업의 isFinished 키 경로를 모니터링해야 한다. 따라서 종료 알림을 생성하지 않는다면 종속된 다른 작업이 실행되지 않을 수 있다.

 

Maintaining KVO Compliance

NSOperation 클래스는 다음 나타나는 키 경로를 준수하는 KVO이다.

 

start 메서드를 override 하거나 그 외의 NSOperation 객체를 직접 정의하려는 경우 객체가 위의 키 경로에 대해 KVO를 준수하는지 확인해야 한다. start 메서드를 override 할 때 가장 중요하게 고려해야 하는 키 경로는 isExecuting, isFinished이다. 이 두 개의 키가 메서드를 새로 구현하면 가장 영향을 많이 받는 키이다.

 

작업 개체 이외의 항목에 대한 종속성을 구현하려면 isReady 메서드를 override 하여 종속성이 충족될 때까지 No를 강제로 반환하도록 할 수 있다. (사용자가 종속성을 정의하는 경우 NSOperation 클래스가 제공하는 default 종속성 관리 시스템을 사용한다면 isReady 메서드에서 super를 호출해야 한다.) 작업의 준비 상태가 변하게 되면 isReady의 키 경로 값에 대해 KVO 알림을 생성하여 변경 사항을 알려야 한다. addDependency: , removeDependency : 메서드를 override 하지 않으면 dependencies에 대한 키 경로 값을 KVO 알림으로 생성할 필요는 없다.

 

NSOperation의 다른 주요 경로에 대해 KVO 알림을 생성할 수는 있지만 반드시 그렇게 할 필요는 없다. 만약 작업을 취소해야 한다면 간단하게 현재 존재하는 취소 메서드로 처리해줘도 된다는 말이다. 마찬가지로 큐에 있는 작업들의 우선순위 정보를 수정할 필요는 거의 없다. 마지막으로 작업이 동시성 상태를 동적으로 변경할 수 없다면 isConcurrent 키 경로에 대한 KVO 알림을 제공할 필요가 없다.

 

이러한 Key-Value Obersving을 사용자가 만든 작업에서 지원하는 방법에 대한 자세한 내용은 Key-Value Observing Programming Guide에서 알아보자.

Customizing the Execution Behavior of an Operation Object

작업 객체의 configuration(구성)은 개발자가 작업을 만들었지만 큐에 아직 넣지 않았을 때 발생한다. 이번 섹션에서 설명하는 구성 타입은 NSOperation을 직접 서브 클래싱 했는지 또는 2개의 기존 서브 클래스를 사용한 지에 관계없이 모든 작업에 대해 적용할 수 있다.

Configuring Interoperation Dependencies

종속성은 작업 객체들의 실행을 순차적으로 만들 수 있는 방법이다. 어떠한 A라는 작업이 B라는 작업에 종속되어있다면 A 작업은 B작업이 완료되기 전엔 실행될 수 없다. 따라서 종속성을 사용하여 1:1 종속성 또는 복잡한 종속성 그래프를 작성할 수 있다.

 

두 개의 작업 객체에 종속성을 부여하기 위해선 NSOperation의 addDependency: 메서드를 사용하면 된다. 이 메서드는 이 메서드를 현재 호출한 작업 객체로부터 매개변수로 받은 target 작업 객체와의 종속성을 만든다. 즉 예를 들어 A 작업 객체가 addDependency: 메서드를 사용해 B 작업 객체에 대한 종속성을 만들면 A 작업은 B 작업이 완료될 때까지 실행될 수 없다. 이러한 종속성은 동일한 큐에서의 조작으로 제한되지 않는다. 작업 객체는 자체 종속성을 관리하기 때문에 작업 간에 종속성을 만들어 서로 다른 큐에 추가하는 것은 허용된다. 하지만 작업들 간에 순환 종속성을 만들게 되면 작업이 실행되지 못하게 되는 오류를 발생시킨다. (순환 종속성의 예로는 A는 B가, B는 C가, C는 A가 끝나야 실행될 수 있는 작업이라면 세 작업 모두 실행될 수 없다. 이러한 경우를 순환 종속성이라고 한다.)

 

작업의 모든 종속된 작업이 실행을 마치면 해당 작업은 실행한 준비가 된 상태가 되며 isReady 메서드의 동작을 정의해놓은 경우 해당 메서드에 따라 조작 준비를 하게 된다. 그렇게 되면 작업 객체가 큐에 있으면 언제든지 해당 작업을 실행할 수 있게 된다. 또한 만약 작업을 수동으로 실행하려는 경우 start 메서드를 호출하여 실행하면 된다.

 

작업을 실행하거나 Operation queue에 추가하기 전에 항상 종속성을 구성해야 한다. 실행하거나 큐에 추가한 후에 종속성을 추가한다면 이 때문에 작업이 실행되지 않을 수 있다.

 

종속성은 객체의 상태가 변할 때마다 보내는 KVO 알림을 보내는 작업 객체에 의존한다. 작업의 동작을 직접 만들었다면 적절한 KVO 알림도 만들어 줘야 종속성과 관련된 오류를 발생시키지 않을 수 있다. 이러한 종속성의 구성에 대해서는 NSOperation Class Reference에서 더 알아보자.

Changing an Operation's Execution Priority

큐에 추가된 작업의 경우 큐에 먼저 존재하던 작업들의 준비상태를 고려하고 서로의 우선순위에 따라 실행 순서가 결정된다. 준비에 대한 여부는 작업의 종속성에 따라 결정되지만 우선순위는 작업 객체 자체의 특성이다. 작업이 우선순위는 default 값으로는 normal이지만 객체의 setQueuePriority: 메서드로 우선순위를 높이거나 낮출 수 있다.

 

우선순위는 같은 Operation queue에 있는 작업들과만 비교된다. 즉 프로그램에 여러 개의 operation queue가 있는 경우 각 큐 마다 독립적으로 추가되어있는 작업들의 우선순위를 비교하는 것이다. 따라서 우선순위가 낮더라도 다른 큐에 있는 우선 순위가 높은 작업보다 먼저 실행될 수도 있다.

 

우선 순위가 종속성을 대체하는 것은 아니다. 우선순위는 해당 operation queue에서 실행할 작업을 선택하는 데 사용된다. 여기서 우선순위보다 중요한 것은 작업의 준비 상태로 우선순위가 높더라도 준비가 되지 않았다면 실행 될 수 없다. 따라서 다른 작업이 완료되기 전엔 작업을 실행하고 싶지 않다면 여전히 종속성을 사용하면 된다.

Changing the Underlying Thread Priority

OS X v10.6 이후 버전에서는 작업 스레드의 실행 우선순위를 구성할 수 있다. 시스템의 스레드 정책은 커널에 의해 자체적으로 관리되지만 일반적으로 우선순위가 높은 스레드는 실행될 기회가 더 많다. 작업 객체에서 스레드 우선순위를 0.0 ~ 1.0 범위의 부동 소수점 값으로 지정할 수 있고 당연한 말이지만 0.0이 가장 낮고 1.0이 가장 높은 우선순위이다. 스레드를 선언할 때 우선순위를 따로 지정해주지 않으면 0.5의 우선순위를 default 값으로 가지게 된다.

 

작업의 스레드 우선순위를 설정하려면 큐에 추가하거나 수동으로 실행하기 전에 작업 객체의 setThreadPriority: 메서드를 호출해야 한다. 이 메서드를 사용하면 작업이 실행될 때가 되면 실행되려는 스레드의 우선순위를 수정하게 된다. 이렇게 수정된 우선순위는 해당 작업이 실행되는 동안에만 유효하다. 이때 다른 코드들은 우선순위로 default 값을 갖고 실행되게 된다. 동시성을 도입한 작업을 작성할 때 start 메서드를 대체하는 경우 스레드 우선순위를 직접 구성해야 한다.

Setting Up a Completion Block

OS X v10.6 이후 버전에서는 main task가 실행을 끝내면 작업이 completion block을 실행할 수 있다. 이러한 완료블록을 사용해서 main task에서 고려되지 않은 어떠한 작업도 수행할 수 있다. 예를 들어 이 블럭을 사용해서 작업이 끝났다는 것을 클라이언트에게 알릴 수 있다. 동시성 작업은 이러한 블럭을 사용하여 최종 KVO 알림을 생성할 수 있다.

 

이러한 완료 블럭을 정의하려면 NSOperation의 setCompletionBlock: 메서드를 사용하면 된다. 이 메서드에 전달되는 블록에는 인수와 반환 값이 없어야 한다.

Tips for Implementing Operation Objects

이러한 방법으로 작업 객체를 구현하기는 쉬워졌지만 실제 작성할 때 알아야 할 몇 가지 사항이 있다. 이번 섹션에서는 작업을 작성할 때 고려해야 할 점을 알아보자

Managing Memory in Operation Objects

이번 섹션에서는 작업 객체에서 올바른 메모리 관리의 중요한 요소를 알아보자. Objective-C 프로그램의 메모리 관리에 대한 정보는 Advanced Memory Management Programming Guides 참고하자.

Avoid Per-Thread Storage

대부분의 작업은 스레드에서 실행되지만 비동시 작업의 경우 스레드는 일반적으로 operation queue에 의해 제공된다. 큐가 스레드를 제공하는 경우 해당 스레드는 큐가 소유하고 작업이 건드리게 되면 안 된다. 특히 이러한 스레드에 작업 객체의 데이터를 연결해서는 안된다. Operation queue가 제어하는 스레드의 경우 시스템과 프로그램의 요구에 따라 왔다갔다 하기 때문에 스레드의 저장공간으로 작업 간의 데이터를 주고받으면 문제가 발생할 수 있다.

 

작업 객체의 경우 스레드의 저장공간을 사용할 이유가 없다. 처음 작업 객체가 만들어질 때 작업에 필요한 모든 것을 제공해야 한다. 따라서 작업 객체는 자체적으로 저장공간을 스스로 제공하게 되고 모든 수신, 발신 데이터는 프로그램에 다시 통합되거나 더 이상 필요하지 않을 때까지 저장하고 있어야 한다.

Keep References to Your Operation Object As Needed

작업이 비동기적으로 실행될 때 마음대로 작업을 만들고 지우는 것을 할 수 있다고 생각하면 안 된다. 그 작업들도 여전히 객체이며 코드에 필요한 참조를 관리하는 것은 개발자에게 달려있다. 이러한 점은 작업이 완료된 후 작업의 결과 데이터를 사용해야 하는 경우 특히 중요하다.

항상 작업에 대한 참조를 유지해야 하는 이유는 나중에 큐에게 해당 작업에 대하여 요청할 기회가 없기 때문이다. 큐는 가능한 빠르게 작업을 디스 패치하고 실행하는데 모든 노력을 기울이며 실제로는 작업이 큐에 추가되면 작업은 바로 실행되게 된다. 큐에 있는 작업에 대해 참조를 얻으려고 하다 보면 이미 작업은 완료되어 제거되어있을 수 있다.

Handling Errors and Exceptions

작업은 본질적으로 프로그램 내부의 개별 entity 이므로 발생하는 모든 오류와 예외를 처리해야 한다. OS X V10.6 이상의 버전에서는 NSOperation 클래스가 제공하는 main 메서드는 예외를 잡아주지 않는다. 즉 자신의 코드는 항상 예외를 직접 처리해야 한다. 이러한 오류를 발견하면 오류를 확인하고 필요에 따라 프로그램에게 알려줘야한다. 만약 start 메서드를 다시 만든다면 예외를 잘 처리하여 기본 스레드의 범위를 벗어나지 않도록 해야한다.

 

처리해야 할 오류에는 다음과 같은 것들이 있다.

 

  • UNIX errno-style error codes
  • 메서드와 함수가 반환한 명시적 오류
  • 자신의 코드나 다른 시스템 프레임워크에서 잡은 예외
  • NSOperation 클래스 자체에서 잡은 예외의 경우는 다음과 같은 경우이다.
    • start 메서드를 호출했을 때 작업이 준비상태가 아닐 때
    • 작업이 실행되고 있거나 끝났는데 start 메서드가 다시 호출될 때
    • 작업이 미디 시작되거나 끝났는데 완료 블록을 추가하려고 할 때
    • 취소된 NSInvocationOperation 객체의 결과에 접근하려고 할 때

직접 만든 코드에 예외 또는 오류가 발생하면 해당 오류를 프로그램에 전파하는데 필요한 단계를 수행해야 한다. NSOperation 클래스는 오류나 예외를 프로그램의 다른 부분으로 전달하기 위한 메서드를 제공하지 않는다. 즉 스스로 이를 전달할 코드를 구현해야 한다.

Determining an Appropriate Scope for Operation Objects

임의로 많은 수의 작업을 Operation queue에 추가할 수 있지만 이러한 것은 비현실적이다. 다른 객체와 마찬가지로 NSOperation 클래스의 인스턴스도 메모리를 소비하고 실행하기 위해선 비용이 든다. 각 작업 객체가 적은 양의 일만 하는데 이를 수천수만 개를 만든다면 실제로 작업을 실행하는 것보다 작업을 디스 패치하는데 더 많은 시간을 소비하고 있다는 것을 알 수 있다. 또한 프로그램에 이미 메모리 제약이 있는 경우 메모리에 수천수만 개의 작업 객체가 있다면 성능이 저하될 수 있다.

 

작업을 효율적으로 실행하는데 필요한 것은 작업량과 컴퓨터 사용률을 적절히 유지하는 것이다. 즉 작업을 합리적인 만큼만 실행해야 한다. 예를 들어 100개의 다른 값에 동일한 작업을 수행하기 위해 100 개의 작업 객체를 만드는 것보다 값을 10개씩 나누어 10개의 작업 객체로 처리하는 것이 더 낫다.

 

또한 많은 수의 작업을 큐에 추가하거나 처리되는 것 보다 빠르게 작업을 큐에 계속 추가하면 안 된다. 큐를 넘치게 만드는 것보단 batch를 만들어 실행하는 게 좋다. 한 batch의 작업이 실행을 끝내면 완료 블록을 사용하여 프로그램에게 새 batch를 작성하라고 지시하는 식으로 구현하자. 할 일이 너무 많아서 컴퓨터를 최대한으로 활용하고 싶어도 메모리 크기에 맞게 작업을 수행해야 한다.

 

물론 생성하는 작업 객체의 수와 각각에서 수행하는 작업의 양은 가변적이며 이는 프로그램에 따라 다르다. 효율성과 속도 사이의 균형을 찾기 위해서는 항상 같은 기기와 도구를 사용해야 한다.

Executing Operations

당연한 말이지만 프로그램은 스스로와 관련 있는 작업을 수행해야 한다. 이번 섹션에서는 작업을 실행하는 몇 가지 방법과 런타임 시 작업의 실행을 조작하는 방법에 대해 알아보자

Adding Operations to an Operation Queue

지금까지 알아본 작업을 실행하는 가장 쉬운 방법은 NSOperationQueue 클래스의 인스턴스를 Operation queue로 실행하는 방법이었다. 프로그램은 사용하려는 operation queue를 생성하고 관리하는데 여러 개의 큐가 있을 수 있지만 특정 시점에 실행 중인 작업 수에는 제한이 있을 수 있다. Operation queue은 시스템과 함께 동시에 실행할 수 있는 작업 수를 사용 가능한 코어 및 시스템 로드에 적합한 값으로 제한한다. 따라서 이러한 큐를 많이 만든다고 해서 작업을 더 많이 실행할 수 있는 것은 아니다.

NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];

위의 코드는 Operation queue를 만드는 방법이다.

 

이러한 큐에 작업을 추가하려면 addOperation: 메서드를 사용하면 된다. OS X v10.6 이상에서는 addOperation: waitUntilFinished: 메서드를 사용하여 작업 객체를 추가하거나 addOperationWithBlock: 메서드를 사용하여 블록 객체를 큐에 직접 추가할 수 있다. 이러한 방법은 작업을 큐에 넣고 실행을 시작해야 한다는 것을 큐에 알린다. 보통은 큐에 추가된 직후에 실행되지만 여러 가지 이유로 실행이 지연될 수 있다. 예를 들어 종속성 때문에 지연될 수도 있고 작업이 이미 취소되었거나 동시에 실행 가능한 작업 수를 이미 실행 중이라면 지연이 될 수 있다.

[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
[aQueue addOperationWithBlock:^{
   /* Do something. */
}];

위의 코드는 큐에 작업을 추가하는 코드이다.

 

작업을 큐에 추가하기 전에 작업 실행에 필요한 것들을 모두 구성하고 수정해줘야 한다. 추가가 이미 된 상태에서는 언제 시작될지 모르기 때문에 미리 해주는 것이 좋다.

 

NSOperationQueue 클래스는 동시에 작업들을 실행하기 위해 설계되었지만 하나의 큐는 하나의 작업만 실행할 수 있다. setMaxConcurrentOperationCount: 메서드는 큐가 동시에 몇 개의 작업을 실행할 수 있는지를 조절할 수 있다. 만약 1이 이 메서드에 전달되면 큐는 한 번에 하나의 작업만 실행한다. 따라서 직렬 Operation queue는 Grand Central Dispatch의 직렬 dispatch queue와 동일한 작동을 하지 않는다. 작업의 실행 순서가 중요한 경우 작업을 큐에 추가하기 전에 종속성을 사용해서 해당 순서를 설계해야 한다.

Executing Operations Manually

Operation queue가 작업 객체를 실행하는 가장 편한 방법이지만 작업들을 큐를 사용하지 않고 직접 사용하는 방법도 있다. 만약 작업을 직접 실행시키고 싶다면 구현 시 주의할 점이 있다. 작업을 실행하려고 할 땐 작업이 항상 준비상태여야 하고 start 메서드를 사용해서 시작해야 한다.

 

isReady 메서드가 YES를 반환할 때까지 작업은 실행할 수 없는 것으로 간주되므로 이러한 메서드는 NSOperation의 종속성 관리 시스템에 통합되어 작업의 종속성 상태를 제공한다. 종속된 작업들이 모두 실행된 경우에만 작업을 시작할 수 있다.

 

작업을 직접 실행할 땐 start 메서드로 시작해야 한다고 했었는데 이는 start 메서드가 실제로 실행을 하기 전에 여러 가지 안전점검을 수행하기 때문에 main 및 다른 메서드를 대신하여 사용된다. start 메서드가 KVO 알림을 생성하는 것도 사용하는 이유 중 하나이다. 만약 작업이 실행되기 전에 이미 취소된 상태라면 실행을 하지 않고 호출됐을 때 준비가 되지 않은 상태라면 오류를 발생시킨다.

 

프로그램에서 동시성 작업을 정의하는 경우 isConcurrent 메서드를 호출하는 것도 고려해봐야 한다. 이 메서드가 NO를 반환하는 경우 코드는 작업을 동기적으로 실행할지 별도의 스레드를 작성하여 실행할지를 결정할 수 있다. 이러한 검사는 개발자가 직접 추가해야 한다.

- (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;
}

위의 코드는 작업을 수동으로 실행하기 전에 확인하는 것들에 대한 예이다. 만약 메서드가 NO를 반환하면 타이머를 사용하여 잠시 후에 이를 다시 호출할 수 있다. 만약 YES를 반환해도 취소될 가능성을 염두하여 타이머를 유지한다.

Cancelling Operations

Operation queue에 한 번 추가가 되면 작업 객체는 큐에 소유되며 제거할 수 없다. 이렇게 큐에 추가된 작업을 제거하는 유일한 방법은 작업을 취소하는 것이다. cancel 메서드를 사용하여 하나의 작업 객체를 취소하거나 cancelAllOperations 메서드를 사용하여 큐의 모든 작업 객체를 취소할 수 있다.

 

작업이 더 이상 필요하지 않을 경우에만 취소해야 한다. cancel 명령을 실행하면 작업이 "canceled" 상태가 되어 실행되지 않는다. 이렇게 취소된 작업은 "finished"로 간주되어 KVO 알림을 받아 제거된다. 이때 종속성에도 영향을 주기 때문에 선택적으로 작업을 제거하기보단 프로그램 종료, 사용자가 취소를 요청하는 것과 같은 이벤트에 응답하여 모든 대기 중인 작업을 취소하는 것이 더 일반적이다.

Waiting for Operations to Finish

최고의 성능을 얻기 위해서는 비동기식으로 작업을 설계해야 하며 작업이 실행되는 동안 프로그램이 추가 작업을 수행할 수 있어야 한다. 작업을 만드는 코드가 작업의 결과도 처리하는 경우 NSOperation의 waitUntilFinished 메서드를 사용하여 작업이 완료될 때까지 해당 코드를 차단할 수 있다. 그러나 이러한 부분을 조절할 수 있다면 이 메서드는 사용하지 않는 것이 좋다. 스레드를 차단하는 것은 편한 방법이지만 코드에 직렬화를 도입하게 되고 동시에 실행될 수 있는 작업의 양을 제한하게 된다.

 

프로그램의 main 스레드에서 작업을 기다리면 안 된다. 이러한 대기는 보조 스레드 또는 다른 작업에서만 수행해야 한다. 기본 스레드를 차단하면 프로그램이 사용자 이벤트에 응답하지 못하고 마치 프로그램이 멈춘 것 같이 보이게 된다. 단일 작업이 완료되는 것을 기다리는 것 외에도 NSOperationQueue의 waitUntilAllOperationsAreFinished 메서드를 호출하여 큐의 모든 작업을 기다릴 수도 있다. 이때 큐에 작업이 추가되면 대기 시간이 계속 늘어나게 된다.

Suspending and Resuming Queues

작업을 일시적으로 중지하고 싶다면 setSuspended: 메서드를 사용하여 해당 Operation queue를 일시 정지할 수 있다. 큐를 중단해도 이미 실행 중인 작업은 중지되지 않는다. 다만 새로운 작업이 예약되지는 않는 것이다. 사용자가 다시 작업을 시작할 것이라고 기대하기 때문에 이러한 중지는 보통 사용자 이벤트에 의해 발생한다.

이번 글은 Apple에서 과거에 올린 글이기 때문에 Objective-C로 코드가 구성되어있는데 Swift로 구현하는 방법은 여기를 참고해주세요~

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함