【问题标题】:Pre-empting NSOperation on one NSOperationQueue with NSOperation placed onto a separate NSOperationQueue?在一个 NSOperationQueue 上抢占 NSOperation 并将 NSOperation 放置到一个单独的 NSOperationQueue 上?
【发布时间】:2012-11-14 03:51:03
【问题描述】:

我有一个应用程序,其中一个长时间运行的进程(> 1 分钟)被放置到 NSOperationQueue(队列 A)上。当队列 A 操作运行时,UI 完全响应,正如预期的那样。

但是,我有一种用户可以执行的不同类型的操作,它在完全独立的 NSOperationQueue(队列 B)上运行。

当 UI 事件触发在队列 B 上放置操作时,它必须等到队列 A 上当前执行的操作完成之后。这发生在 iPod Touch (MC544LL) 上。

我希望看到的是,放置在队列 B 上的任何操作都会或多或少地立即开始与队列 A 上的操作并行执行。这是我在模拟器上看到的行为。

我的问题分为两部分:

  • 我在设备上看到的行为是否符合可用文档的预期?
  • 使用 NSOperation/NSOperationQueue,如何使用队列 B 上的新操作抢占队列 A 上当前正在运行的操作?

注意:通过将 GCD 队列用于队列 A/B,我可以准确地获得我想要的行为,因此我知道我的设备能够支持我正在尝试做的事情。但是,我真的非常想使用 NSOperationQueue,因为这两个操作都需要取消。

我有一个简单的测试应用程序:

ViewController 是:

//
//  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}

如您所见,第二个操作直到第一个操作完成后才开始执行,尽管它们是两个独立的(并且可能是独立的)NSOperationQueues。

我已经阅读了Apple Concurrency Guide,但没有找到任何描述这种情况的信息。我还阅读了有关相关主题的两个 SO 问题(linklink),但似乎都没有触及我看到的问题的核心(先发制人)。

我尝试过的其他事情:

  • 在每个 NSOperation 上设置 queuePriority
  • 在每个 NSOperation 上设置 queuePriority,同时将两种类型的操作放在同一个队列中
  • 将两个操作放在同一个队列中

此问题经过多次修改,可能会使某些 cmets/答案难以理解。

【问题讨论】:

  • 我看到你在我输入答案时治愈了症状 :-) 我怀疑如果你需要 NSOperationQueue 的操作管理工具并且不能使用,那么将慢速操作队列串行化会满足你的需要GCD。
  • @SimonLawrence,是的 - 我们的更新跨越了路径:)。不幸的是,使用 [slowQueue setMaxConcurrentOperationCount:1] 使队列串行并没有帮助。还有其他方法吗?
  • 在您问题的代码中,您将两个操作添加到同一个队列中。错字,还是你所有问题的原因?
  • 我尝试过的一个变体留下的错字。将两个操作添加到相同或不同队列的行为相同。
  • 嗨 Rich,您是否尝试过使用 NSOperation 子类在不使用 GCD 的情况下运行块的建议(请参阅下面的 PseudoBlockOperation)?这应该可以满足您对长时间运行操作的需求,并且可能比 NSBlockOperation 更适合这些操作。

标签: objective-c ios concurrency


【解决方案1】:

我怀疑您遇到的问题是两个操作队列都在底层默认优先级调度队列上执行它们的块。因此,如果在快速操作之前将几个慢速操作排入队列,那么您可能会看到这种行为。

为什么不为慢速操作设置 NSOperationQueue 实例,使其在任何给定时间只执行一个操作(即将此队列的 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);
    });
}

至于根据您的 cmets 使用 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];
}

我认为您添加到慢速队列操作的两个块正在默认队列上并行执行,并阻止您的快速操作被安排。

编辑:

如果您仍然发现默认的 GCD 队列令人窒息,为什么不创建一个 NSOperation 子类来执行块而不使用 GCD 来处理您的缓慢操作,这仍然会为您提供不创建单独子类的声明性便利每个操作,但使用常规 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];

请注意,在此示例中,添加到同一操作的任何块都将按顺序执行而不是并发执行,以并发执行每个块创建一个操作。这比 NSBlockOperation 的优势在于您可以通过更改 BlockOperation 的定义将参数传递到块中 - 这里我传递了包含操作,但您可以传递任何其他需要的上下文。

希望对您有所帮助。

【讨论】:

  • 这是朝着正确方向迈出的一步,但正如我的编辑(与您的答案交叉)所示,我需要能够取消所有操作。据我所知,这对于内置 GCD 功能是不可能的。
  • 我刚刚在您的示例中注意到,您将两个慢块添加到同一个 NSOperation,我相信这意味着即使 NSOperationQueue 现在设置为串行,它们也会在 GCD 队列上并行退出,您可以尝试添加 setMaxConcurrentOperations = 1 修改 loadSlowQueue 以便它为每个块添加一个操作,而不是将它们都放在同一个操作中?
  • 在 viewDidLoad 中添加 [self.slowQueue setMaxConcurrentOperationCount:1] 得到的结果与发布的相同。主要问题仍然存在:fastQueue 被完全阻塞,直到 slowQueue 上的工作完成。这两个队列似乎并不独立。
  • 您是否也尝试过不将多个块添加到慢速队列的同一操作中(请参阅上面对答案的编辑)?如果您将两个块添加到一个操作中,它们可以在底层 GCD 队列上同时执行,而如果您在串行 NSOperationQueue 上为每个块添加一个 NSOperation,那么该队列的操作和块将按顺序执行,这应该允许您的另一个队列运行。
  • 我刚刚注意到您使用新的建议 loadSlowQueue 方法更新了您的答案。它的工作原理是使快速操作能够在两个慢速操作之间运行。但是,主要问题是在慢块 1 完成执行之前,fastQueue 上没有任何操作正在运行。在我的情况下,这可能需要一分钟或更长时间才能让任何快速操作开始执行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多