2

長時間実行されるプロセス (> 1 分) が NSOperationQueue (キュー A) に配置されるアプリケーションがあります。キュー A 操作の実行中、UI は期待どおりに完全に応答します。

ただし、ユーザーが実行できる別の種類の操作があり、完全に別の NSOperationQueue (キュー B) で実行されます。

UI イベントがキュー B で操作の配置をトリガーする場合、キュー A で現在実行中の操作が終了するまで待機する必要があります。これは、iPod Touch (MC544LL) で発生します。

代わりに期待していたのは、キュー B に配置された操作は、多かれ少なかれすぐにキュー A の操作と並行して実行を開始するということでした。これが、シミュレーターで見られる動作です。

私の質問は2つの部分です:

  • デバイスで見られる動作は、利用可能なドキュメントに基づいて予想されるものですか?
  • NSOperation/NSOperationQueue を使用して、キュー A で現在実行中の操作を、キュー B に配置された新しい操作でプリエンプトするにはどうすればよいですか?

注: キュー A/B に GCD キューを使用することで、目的の動作を正確に取得できるため、自分のデバイスが私がやろうとしていることをサポートできることがわかります。ただし、両方の操作をキャンセル可能にする必要があるため、私は本当に NSOperationQueue を使用したいと考えています。

私は簡単なテストアプリケーションを持っています:

ここに画像の説明を入力

ビューコントローラーは次のとおりです。

//
//  ViewController.m
//  QueueTest
//

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) NSOperationQueue *slowQueue;
@property (strong, nonatomic) NSOperationQueue *fastQueue;

@end

@implementation ViewController

-(id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder]) {
        self.slowQueue = [[NSOperationQueue alloc] init];
        self.fastQueue = [[NSOperationQueue alloc] init];
    }

    return self;
}

-(void)viewDidLoad
{
    NSLog(@"View loaded on thread %@", [NSThread currentThread]);
}

// Responds to "Slow Op Start" button
- (IBAction)slowOpStartPressed:(id)sender {
    NSBlockOperation *operation = [[NSBlockOperation alloc] init];

    [operation addExecutionBlock:^{
        [self workHard:600];
    }];

    [self.slowQueue addOperation:operation];
}

// Responds to "Fast Op Start" button
- (IBAction)fastOpStart:(id)sender {    
    NSBlockOperation *operation = [[NSBlockOperation alloc] init];

    [operation addExecutionBlock:^{
        NSLog(@"Fast operation on thread %@", [NSThread currentThread]);
    }];

    [self.fastQueue addOperation:operation];
}

-(void)workHard:(NSUInteger)iterations
{
    NSLog(@"SlowOperation start on thread %@", [NSThread currentThread]);

    NSDecimalNumber *result = [[NSDecimalNumber alloc] initWithString:@"0"];

    for (NSUInteger i = 0; i < iterations; i++) {        
        NSDecimalNumber *outer = [[NSDecimalNumber alloc] initWithUnsignedInteger:i];

        for (NSUInteger j = 0; j < iterations; j++) {
            NSDecimalNumber *inner = [[NSDecimalNumber alloc] initWithUnsignedInteger:j];
            NSDecimalNumber *product = [outer decimalNumberByMultiplyingBy:inner];

            result = [result decimalNumberByAdding:product];
        }

        result = [result decimalNumberByAdding:outer];
    }

    NSLog(@"SlowOperation end");
}

@end

最初に「Slow Op Start」ボタンを押してから約 1 秒後に「Fast Op Start」ボタンを押した後に表示される出力は次のとおりです。

2012-11-28 07:41:13.051 QueueTest[12558:907] View loaded on thread <NSThread: 0x1d51ec30>{name = (null), num = 1}
2012-11-28 07:41:14.745 QueueTest[12558:1703] SlowOperation start on thread <NSThread: 0x1d55e5f0>{name = (null), num = 3}
2012-11-28 07:41:25.127 QueueTest[12558:1703] SlowOperation end
2012-11-28 07:41:25.913 QueueTest[12558:3907] Fast operation on thread <NSThread: 0x1e36d4c0>{name = (null), num = 4}

ご覧のとおり、2 番目の操作は、最初の操作が終了するまで実行を開始しません。これらは 2 つの別個の (おそらく独立した) NSOperationQueue であるという事実にもかかわらずです。

Apple Concurrency Guideを読みましたが、この状況を説明するものは何も見つかりません。また、関連するトピックに関する 2 つの SO の質問 ( linklink ) も読みましたが、どちらも私が見ている問題 (プリエンプション) の核心には達していないようです。

私が試した他のこと:

  • 各 NSOperation に queuePriority を設定する
  • 両方のタイプの操作を同じキューに配置しながら、各 NSOperation に queuePriority を設定する
  • 両方の操作を同じキューに配置する

この質問は複数回編集されているため、特定のコメント/回答が理解しにくい場合があります。

4

1 に答える 1

1

あなたが抱えている問題は、両方の操作キューが、基になるデフォルトの優先ディスパッチキューでブロックを実行していることだと思います。したがって、高速操作の前にいくつかの低速操作がキューに入れられている場合は、おそらくこの動作が表示されます。

低速操作用にNSOperationQueueインスタンスを設定して、常に1つの操作のみを実行するようにする(つまり、このキューに対してmaxConcurrentOperationCountを1に設定する)か、操作がすべてブロックである場合は、GCDキューを直接使用しないのはなぜですか?例えば

static dispatch_queue_t slowOpQueue = NULL;
static dispatch_queue_t fastOpQueue = NULL;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    slowOpQueue = dispatch_queue_create("Slow Ops Queue", NULL);
    fastOpQueue = dispatch_queue_create("Fast Ops Queue", DISPATCH_QUEUE_CONCURRENT);
});

for (NSUInteger slowOpIndex = 0; slowOpIndex < 5; slowOpIndex++) {
    dispatch_async(slowOpQueue, ^(void) {
        NSLog(@"* Starting slow op %d.", slowOpIndex);
        for (NSUInteger delayLoop = 0; delayLoop < 1000; delayLoop++) {
            putchar('.');
        }

        NSLog(@"* Ending slow op %d.", slowOpIndex);
    });
}

for (NSUInteger fastBlockIndex = 0; fastBlockIndex < 10; fastBlockIndex++) {
    dispatch_async(fastOpQueue, ^(void) {
        NSLog(@"Starting fast op %d.", fastBlockIndex);
        NSLog(@"Ending fast op %d.", fastBlockIndex);
    });
}

操作キャンセル機能などの必要性についてのコメントに従ってNSOperationQueueを使用する限り、次のことを試すことができます。

- (void)loadSlowQueue
{
    [self.slowQueue setMaxConcurrentOperationCount:1];
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"begin slow block 1");

        [self workHard:500];

        NSLog(@"end slow block 1");
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"begin slow block 2");

        [self workHard:500];

        NSLog(@"end slow block 2");
    }];

    [self.slowQueue addOperation:operation];
    [self.slowQueue addOperation:operation2];
}

遅いキューでの操作に追加する2つのブロックは、デフォルトのキューで並行して実行されており、速い操作がスケジュールされないようになっていると思います。

編集:

それでもデフォルトのGCDキューが詰まっていることがわかっている場合は、低速の操作にGCDをまったく使用せずにブロックを実行するNSOperationサブクラスを作成しません。これにより、操作ごとに個別のサブクラスを作成しないという宣言型の利便性が得られますが、通常のNSOperationのスレッドモデルを使用します。例えば

#import <Foundation/Foundation.h>

typedef void (^BlockOperation)(NSOperation *containingOperation);

@interface PseudoBlockOperation : NSOperation

- (id)initWithBlock:(BlockOperation)block;
- (void)addBlock:(BlockOperation)block;

@end

そして、実装のために:

#import "PseudoBlockOperation.h"

@interface PseudoBlockOperation()

@property (nonatomic, strong) NSMutableArray *blocks;

@end

@implementation PseudoBlockOperation

@synthesize blocks;

- (id)init
{
    self = [super init];

    if (self) {
        blocks = [[NSMutableArray alloc] initWithCapacity:1];
    }

    return self;
}

- (id)initWithBlock:(BlockOperation)block
{
    self = [self init];

    if (self) {
        [blocks addObject:[block copy]];
    }

    return self;
}

- (void)main
{
    @autoreleasepool {
        for (BlockOperation block in blocks) {
            block(self);
        }
    }
}

- (void)addBlock:(BlockOperation)block
{
    [blocks addObject:[block copy]];
}

@end

次に、コードで次のようなことを行うことができます。

PseudoBlockOperation *operation = [[PseudoBlockOperation alloc] init];
[operation addBlock:^(NSOperation *operation) {
    if (!operation.isCancelled) {
        NSLog(@"begin slow block 1");

        [self workHard:500];

        NSLog(@"end slow block 1");
    }
}];

[operation addBlock:^(NSOperation *operation) {
    if (!operation.isCancelled) {
        NSLog(@"begin slow block 2");

        [self workHard:500];

        NSLog(@"end slow block 2");
    }
}];

[self.slowQueue addOperation:operation];

この例では、同じ操作に追加されたブロックは同時に実行されるのではなく順番に実行され、同時に実行されると、ブロックごとに1つの操作が作成されることに注意してください。これには、BlockOperationの定義を変更することでパラメーターをブロックに渡すことができるという点で、NSBlockOperationよりも優れています。ここでは、包含操作を渡しましたが、他の必要なコンテキストはすべて渡すことができます。

お役に立てば幸いです。

于 2012-11-26T18:30:53.550 に答える