LoginSignup
10
8

More than 5 years have passed since last update.

実行中のNSOperationに考慮しつつ、NSOperationQueueのキャンセルを実行する

Last updated at Posted at 2015-02-01

NSOperationQueuecancelAllOperationを呼び出せば、全ての処理を止めることが可能ですが、キューに追加したNSOperationの処理によっては、すぐにキャンセルできない場合があります。

例えば、下のようなケースの場合などです。

  • ダウンロードしたデータをパースした後、Core Dataのデータインポート処理。すぐにキャンセルすると、データの不整合が発生するため、実行中の処理は終わるまで待たせる。

それを実現するには、下のようにNSOperationQueueのサブクラスを作成し、実行中の処理が終わるまで待ち、終わったら完了のブロックを実行するメソッドを作ればよいでしょう。

// DataParseOperationQueue.h
#import <Foundation/Foundation.h>

@interface DataParseOperationQueue : NSOperationQueue

// XXX: 全ての実行中の`NSOperation`が終わるまで待ち、完了したらcompletionHandler blocksを実行する
- (void)cancelAllOperationsWithCompletionHandler:(void(^)(void))completionHandler;

@end

// DataParseOperationQueue.m
#import "DataParseOperationQueue.h"

static NSString *const SynchronizedQueueLabelPrefix = @"com.rh7.synchronized.DataParseOperationQueue";

@interface ParseOperationQueue ()

// XXX: 同時アクセスを防止するためのシリアルキュー(@synchronizedでも可)
@property (nonatomic) dispatch_queue_t synchronizedQueue;

@end

@implementation ParseOperationQueue

- (instancetype)init {
    self = [super init];
    if (self) {
        const char *synchronized_queue_label = [[SynchronizedQueueLabelPrefix stringByAppendingPathExtension:[[NSUUID UUID] UUIDString]] UTF8String];
        _synchronizedQueue = dispatch_queue_create(synchronized_queue_label, DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)cancelAllOperationsWithCompletionHandler:(void(^)(void))completionHandler {
    dispatch_sync(self.synchronizedQueue, ^{
        // XXX: 実行待ちの`NSOperation`を実行されないように停止する
        self.suspended = YES;
        // XXX: 実行待ちの`NSOperation`は全てキャンセルする
        NSArray *pendingOperations = [self.operations filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"%K == %@", @"isExecuting", @NO]];
        [pendingOperations enumerateObjectsUsingBlock:^(NSOperation *op, NSUInteger idx, BOOL *stop) {
            [op cancel];
        }];

        // XXX: 実行中の`NSOperation`が全て完了してから、`completionHandler`を一番最後に実行するため、同時実行並列数を1にする
        NSUInteger currentMaxConcurrentCount = self.maxConcurrentOperationCount;
        self.maxConcurrentOperationCount = 1;
        // XXX: 再開
        self.suspended = NO;
        // XXX: このOperationは必ず最後に実行される
        NSBlockOperation *waitForCompletion = [NSBlockOperation blockOperationWithBlock:^{
            if (completionHandler) {
                completionHandler();    // 呼び出し元から渡されたblocksを実行
            }
            // XXX: 設定を元に戻す
            self.maxConcurrentOperationCount = currentMaxConcurrentCount;
        }];
        [self addOperation:waitForCompletion];
    });
}

@end

このOperationQueueを使って、処理を実行するManagerクラスを定義した場合、キャンセル処理下記のようになります。

#import "DataParseOperationQueue.h"

@interface AccountManger ()

@property (nonatomic, readonly) DataParseOperationQueue *operationQueue;

@end

@implementation AccountManager

- (void)cancelAllOperationsWithCompletionHandler:(void(^)(void))completionHandler {
    dispatch_sync(self.synchronizedQueue, ^{
        DataParseOperationQueue *operationQueue = self.operationQueue;
        self.operationQueue = nil;      // XXX: 新しい処理を追加されないように、 一旦他から参照させないようにする
        // XXX: Pendingのままにして、実行していないオペレーションをすべてキャンセルする
        [operationQueue cancelAllOperationsWithCompletionHandler:^{
            // XXX: 元に戻す
            self.operationQueue = operationQueue;
            if (completionHandler) {
                completionHandler();
            }
        }];
    });
}

@end

さらに、ログアウト時にNSOperationQueueに追加されている処理が空っぽになるまで待つようにする場合は、下記のような感じになるでしょう。PromiseKitは、複数の処理を同時に実行して待つことが可能だから使ってます。

- (PMKPromise *)waitAllOperations {
    PMKPromise *accountManagerCancelAllOperations = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
        [[AccountManager defaultManager] cancelAllOperationsWithCompletionHandler:^{ fulfill(nil); }];
    }];
    PMKPromise *apiResourceManagerCancelAllOperations = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
        [[APIResourceManager defaultManager] cancelAllOperationsWithCompletionHandler:^{ fulfill(nil); }];
    }];
    return [PMKPromise when:@[accountManagerCancelAllOperations, organizationCancelAllOperations]];
}

- (void)logout {
    [APISessionManager sharedManager] resetSession];
    [self waitAllOperations].finally(^{
        // XXX: すべてのOperationQueueのキャンセルが完了したら、ログアウト処理を実行する。
        [self closeSessionWithNotification];
    });
}

参考

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8