18

このトピックについて多くの混乱があり、結果として NSOperation サブクラスのデバッグに数時間を費やしたため、この質問を投稿しました。

問題は、非同期コールバックが完了するまで実際には完了しない非同期メソッドを実行すると、NSOperation があまり役に立たないことです。

NSOperation 自体がコールバック デリゲートである場合、コールバックが別のスレッドで発生するため、操作を適切に完了するのに十分ではない場合もあります。

メイン スレッドで NSOperation を作成し、それを NSOperationQueue に追加すると、NSOperation 内のコードが非同期呼び出しを起動し、AppDelegate またはビュー コントローラーのメソッドにコールバックするとします。

メイン スレッドをブロックできないか、UI がロックされるため、2 つのオプションがあります。

1) NSOperation を作成し、次の署名を使用して NSOperationQueue に追加します。

[NSOperationQueue addOperations:@[myOp] waitUntilFinished:?]

頑張ってください。非同期操作は通常ランループを必要とするため、NSOperation をサブクラス化するかブロックを使用しない限り機能しませんが、コールバックが終了したときに NSOperation を「完了する」必要がある場合は、ブロックでさえ機能しません。

したがって...次のようなもので NSOperation をサブクラス化して、コールバックが操作の完了を通知できるようにします。

//you create an NSOperation subclass it includes a main method that
//keeps the runloop going as follows
//your NSOperation subclass has a BOOL field called "complete"

-(void) main
{

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //I do some stuff which has async callbacks to the appDelegate or any other class (very common)

    while (!complete && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

}

//I also have a setter that the callback method can call on this operation to 
//tell the operation that its done, 
//so it completes, ends the runLoop and ends the operation

-(void) setComplete {
    complete = true;
}

//I override isFinished so that observers can see when Im done
// - since my "complete" field is local to my instance

-(BOOL) isFinished
{
    return complete;
}

OK - これは絶対に機能しません - 邪魔にならないようにしました!

2)このメソッドの 2 番目の問題は、runLoops を適切に終了する必要がある場合 (または、コールバックの外部メソッド呼び出しから実際に終了する場合) に、上記が実際に機能した (実際には機能しない) ことです。

これを呼び出すときに、メイン スレッドで 2 番目の Im を想定してみましょう。UI を少しロックして何も描画しないようにする場合を除き、NSOperationQueue addOperation メソッドで「waitUntilFinished:YES」とは言えません...

では、メイン スレッドをロックせずに、waitUntilFinished:YES と同じ動作を実現するにはどうすればよいでしょうか。

Cocoa での runLoops、NSOperationQueues、Asynch の動作に関して非常に多くの質問があるため、この質問に対する回答として私の解決策を投稿します。

meta.stackoverflow を確認したところ、これは許容可能であり、推奨されているため、私は自分の質問にのみ答えていることに注意してください。コールバック。(他のスレッドでのコールバック)

4

3 に答える 3

16

問題#1への答え

メインメソッドで非同期操作を呼び出す NSOperation があり、操作の外部でコールバックするため、操作が完了したことを伝えて NSOperation を終了する必要があります。

次のコードは上記を修正したものです

//you create an NSOperation subclass it includes a main method that
//keeps the runloop going as follows
//your NSOperation subclass has a BOOL field called "complete"
//ADDED: your NSOperation subclass has a BOOL field called "stopRunLoop"
//ADDED: your NSOperation subclass has a NSThread * field called "myThread"
-(void) main
{
    myThread = [NSThread currentThread];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //I do some stuff which has async callbacks to the appDelegate or any other class (very common)

    while (!stopRunLoop && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

    //in an NSOperation another thread cannot set complete 
    //even with a method call to the operation
    //this is needed or the thread that actually invoked main and 
    //KVO observation will not see the value change
    //Also you may need to do post processing before setting complete.
    //if you just set complete on the thread anything after the 
    //runloop will not be executed.
    //make sure you are actually done.

    complete = YES;

}


-(void) internalComplete
{
    stopRunloop = YES;
}

//This is needed to stop the runLoop, 
//just setting the value from another thread will not work,
//since the thread that created the NSOperation subclass 
//copied the member fields to the
//stack of the thread that ran the main() method.

-(void) setComplete {
    [self performSelector:@selector(internalComplete) onThread:myThread withObject:nil      waitUntilDone:NO];
}

//override isFinished same as before
-(BOOL) isFinished
{
    return complete;
}

問題 2 への回答- 使用できません

[NSOperationQueue addOperations:.. waitUntilFinished:YES]

メインスレッドは更新されませんが、この NSOperation が完了するまで実行してはならないいくつかの OTHER 操作もあり、それらのいずれもメインスレッドをブロックしないためです。

入る...

dispatch_semaphore_t

メイン スレッドから起動する必要がある複数の依存 NSOperations がある場合は、ディスパッチ セマフォを NSOperation に渡すことができます。これらは NSOperation メイン メソッド内の非同期呼び出しであるため、NSOperation サブクラスはそれらのコールバックが完了するまで待機する必要があることに注意してください。 . また、コールバックからのメソッド チェーンも問題になる可能性があります。

メイン スレッドからセマフォを渡すことで、[NSOperation addOperations:... waitUntilFinished: NO] を使用できますが、コールバックがすべて完了するまで他の操作を実行できません。

NSOperation を作成するメイン スレッドのコード

//only one operation will run at a time
dispatch_semaphore_t mySemaphore = dispatch_semaphore_create(1);

//pass your semaphore into the NSOperation on creation
myOperation = [[YourCustomNSOperation alloc] initWithSemaphore:mySemaphore] autorelease];

//call the operation
[myOperationQueue addOperations:@[myOperation] waitUntilFinished:NO];

...NSOperation のコード

//In the main method of your Custom NSOperation - (As shown above) add this call before
//your method does anything
//my custom NSOperation subclass has a field of type dispatch_semaphore_t
//named  "mySemaphore"

-(void) main
{
    myThread = [NSThread currentThread];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //grab the semaphore or wait until its available
    dispatch_semaphore_wait(mySemaphore, DISPATCH_TIME_FOREVER);

    //I do some stuff which has async callbacks to the appDelegate or any other class (very common)

    while (!stopRunLoop && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

    //release the semaphore
    dispatch_semaphore_signal(mySemaphore);

    complete = YES;

}

別のスレッドのコールバック メソッドが NSOperation で setComplete を呼び出すと、3 つのことが起こります。

  1. runloop が停止され、NSOperation が完了できるようになります (それ以外の場合は完了しません)。

  2. セマフォが解放され、セマフォを共有する他の操作を実行できるようになります

  3. NSOperation が完了し、割り当てが解除されます

方法 2 を使用すると、NSOperationQueue から呼び出された任意の非同期メソッドを待機でき、それらが実行ループを完了することを認識でき、メイン スレッドをブロックすることなく、好きな方法でコールバックをチェーンできます。

于 2012-09-05T16:19:04.703 に答える
6

これらのアプローチは a) あまりにも複雑であり、b) NSOperation を使用するように設計された方法で使用していないため、これらの回答を詳細には読みませんでした。皆さんは、すでに存在する機能をハッキングしているようです。

解決策は、NSOperation をサブクラス化し、getter isConcurrent をオーバーライドして YES を返すことです。次に - (void)start メソッドを実装し、非同期タスクを開始します。つまり、タスクが完了したことを NSOperationQueue が認識できるように、isFinished および isExecuting で KVO 通知を生成する必要があります。

(更新: NSOperation をサブクラス化する方法は次のとおりです) (更新 2: バックグラウンド スレッドで作業するときに NSRunLoop を必要とするコードがある場合に、NSRunLoop を処理する方法を追加しました。たとえば、Dropbox Core API)

// HSConcurrentOperation : NSOperation
#import "HSConcurrentOperation.h"  

@interface HSConcurrentOperation()
{
@protected

    BOOL _isExecuting;
    BOOL _isFinished;

    // if you need run loops (e.g. for libraries with delegate callbacks that require a run loop)
    BOOL _requiresRunLoop;
    NSTimer *_keepAliveTimer;  // a NSRunLoop needs a source input or timer for its run method to do anything.
    BOOL _stopRunLoop;
}
@end

@implementation HSConcurrentOperation

- (instancetype)init
{
    self = [super init];
    if (self) {
        _isExecuting = NO;
        _isFinished = NO;

    }
    return self;
}

- (BOOL)isConcurrent
{
    return YES;
}

- (BOOL)isExecuting
{
    return _isExecuting;
}

- (BOOL)isFinished
{
    return _isFinished;
}

- (void)start
{

    [self willChangeValueForKey:@"isExecuting"];
    NSLog(@"BEGINNING: %@", self.description);
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];

    _requiresRunLoop = YES;  // depends on your situation.
    if(_requiresRunLoop)
    {
       NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

       // run loops don't run if they don't have input sources or timers on them.  So we add a timer that we never intend to fire and remove him later.
       _keepAliveTimer = [NSTimer timerWithTimeInterval:CGFLOAT_MAX target:self selector:@selector(timeout:) userInfo:nil repeats:nil];
       [runLoop addTimer:_keepAliveTimer forMode:NSDefaultRunLoopMode];

       [self doWork];

       NSTimeInterval updateInterval = 0.1f;
       NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
       while (!_stopRunLoop && [runLoop runMode: NSDefaultRunLoopMode beforeDate:loopUntil])
       {
           loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
       }

    }
    else
    {
      [self doWork];
    }
}

- (void)timeout:(NSTimer*)timer
{
    // this method should never get called.

    [self finishDoingWork];
}

- (void)doWork
{
    // do whatever stuff you need to do on a background thread.
    // Make network calls, asynchronous stuff, call other methods, etc.

    // and whenever the work is done, success or fail, whatever
    // be sure to call finishDoingWork.

    [self finishDoingWork];
}

- (void)finishDoingWork
{
   if(_requiresRunLoop)
   {
      // this removes (presumably still the only) timer from the NSRunLoop
      [_keepAliveTimer invalidate];
      _keepAliveTimer = nil;

      // and this will kill the while loop in the start method
      _stopRunLoop = YES;
   }

   [self finish];

}
- (void)finish
{
    // generate the KVO necessary for the queue to remove him
    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];

}

@end
于 2014-03-26T13:43:54.703 に答える