6

ドキュメントから:

指定した完了ブロックは、isFinished メソッドによって返された値が YES に変わると実行されます。したがって、このブロックは、操作のプライマリ タスクが終了またはキャンセルされた後に、操作オブジェクトによって実行されます。

それが重要な場合、私は を使用しRestKit/AFNetworkingています。

に複数の依存関係がNSOperationありOperationQueueます。完了ブロックを使用して、子供が必要とするいくつかの変数を設定します (結果を配列に追加します)。

(タスク 1,...,タスク N) -> タスク A

taskA addDependency: task1-taskN

完了ブロックが起動される前に子が実行できるため、taskA は不完全なデータを受け取りますか?

参照

NSOperations とその completionBlocks は同時に実行されますか?

完了ブロックにスリープを追加して簡単なテストを行ったところ、別の結果が得られました。完了ブロックはメイン スレッドで実行されます。すべての完了ブロックがスリープしている間に、子タスクが実行されました。

4

2 に答える 2

5

以下の「いくつかの観察事項」で説明するように、この最終的な依存操作が、他の雑多な AFNetworking 完了ブロックが完了する前に開始されないという保証はありません。この最終操作がこれらの完了ブロックが終了するのを本当に待つ必要がある場合は、いくつかの選択肢があります。

  1. n 個の完了ブロックのそれぞれでセマフォを使用して、完了時に信号を送り、完了操作をn 個の信号で待機させます。また

  2. この最終操作を前もってキューに入れずに、個々のアップロードの完了ブロックで、未完了の保留中のアップロードの数を追跡し、それがゼロになったら、最後の「投稿」操作を開始します。

  3. コメントで指摘したように、AFNetworking 操作の呼び出しとその完了ハンドラーを独自の操作でラップすることができ、その時点で標準addDependencyメカニズムを使用できます。

  4. アプローチを放棄することができます(この操作が依存している操作addDependencyのキーにオブザーバーを追加し、それらの依存関係がすべて解決されると、 KVN を実行します。問題は、完了ブロックが完了する前に理論的に発生する可能性があることです)。独自のロジックに置き換えます。たとえば、独自のキー依存関係を追加して完了ブロックで手動で削除できる post 操作があったとします。したがって、カスタム操作isFinishedisReadyisReadyisFinished

    @interface PostOperation ()
    @property (nonatomic, getter = isReady) BOOL ready;
    @property (nonatomic, strong) NSMutableArray *keys;
    @end
    
    @implementation PostOperation
    
    @synthesize ready = _ready;
    
    - (void)addKeyDependency:(id)key {
        if (!self.keys)
            self.keys = [NSMutableArray arrayWithObject:key];
        else
            [self.keys addObject:key];
    
        self.ready = NO;
    }
    
    - (void)removeKeyDependency:(id)key {
        [self.keys removeObject:key];
    
        if ([self.keys count] == 0)
            self.ready = YES;
    }
    
    - (void)setReady:(BOOL)ready {
        if (ready != _ready) {
            [self willChangeValueForKey:@"isReady"];
            _ready = ready;
            [self didChangeValueForKey:@"isReady"];
        }
    }
    
    - (void)addDependency:(NSOperation *)operation{
        NSAssert(FALSE, @"You should not use addDependency with this custom operation");
    }
    

    次に、アプリ コードは、addKeyDependencyではなくを使用して、またはを完了ブロックでaddDependency明示的に使用して、次のようなことを行うことができます。removeKeyDependencycancel

    PostOperation *postOperation = [[PostOperation alloc] init];
    
    for (NSInteger i = 0; i < numberOfImages; i++) {
        NSURL *url = ...
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSString *key = [url absoluteString]; // or you could use whatever unique value you want
    
        AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
        [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
            // update your model or do whatever
    
            // now inform the post operation that this operation is done
    
            [postOperation removeKeyDependency:key];
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            // handle the error any way you want
    
            // perhaps you want to cancel the postOperation; you'd either cancel it or remove the dependency
    
            [postOperation cancel];
        }];
        [postOperation addKeyDependency:key];
        [queue addOperation:operation];
    }
    
    [queue addOperation:postOperation];
    

    これは を使用してAFHTTPRequestOperationおり、アップロードに適した AFNetworking 操作でこのロジックをすべて置き換えることは明らかですが、うまくいけばアイデアが示されます。


元の答え:

いくつかの観察:

  1. あなたが結論付けたと思うように、操作が完了すると、(a) 完了ブロックが開始されます。maxConcurrentOperationCount(b) キューを他の操作 (または操作間の依存関係のためにまだ開始されていない操作) で使用できるようにします。次の操作が開始される前に完了ブロックが完了するという保証はないと思います。

    経験的に、依存操作は完了ブロックが完了するまで実際にはトリガーされないように見えますが、(a) どこにも文書化されていないこと、(b) AFNetworking 独自の を使用している場合はsetCompletionBlockWithSuccess終了するため、これは議論の余地があります。ブロックをメイン キュー (または定義されたsuccessCallbackQueue) に非同期的にディスパッチすることにより、(文書化されていない) 同期の保証を妨害します。

  2. さらに、完了ブロックはメインスレッドで実行されると言います。組み込みの完了ブロックについて話している場合、NSOperationそのような保証はありません。実際、setCompletionBlock ドキュメントには次のように書かれています:

    完了ブロックの正確な実行コンテキストは保証されていませんが、通常はセカンダリ スレッドです。したがって、このブロックを使用して、非常に特殊な実行コンテキストを必要とする作業を行うべきではありません。代わりに、その作業をアプリケーションのメイン スレッドまたはそれを実行できる特定のスレッドにシャントする必要があります。たとえば、操作の完了を調整するためのカスタム スレッドがある場合、完了ブロックを使用してそのスレッドに ping を実行できます。

    しかし、AFNetworking のカスタム補完ブロックの 1 つについて話している場合、たとえばAFHTTPRequestOperation'ssetCompletionBlockWithSuccessで設定する可能性があるブロックについて話している場合は、そうです、それらは一般的にメイン キューにディスパッチされます。しかし、AFNetworking は標準のcompletionBlockメカニズムを使用してこれを行うため、上記の懸念事項は引き続き適用されます。

于 2013-09-11T17:47:49.040 に答える
2

NSOperationAFHTTPRequestOperation のサブクラスであるかどうかは重要です。AFHTTPRequestOperation は、method で独自の目的のためにNSOperationのプロパティを使用します。その場合、プロパティを自分で設定しないでください。completionBlocksetCompletionBlockWithSuccess:failurecompletionBlock

AFHTTPRequestOperation の成功と失敗のハンドラーは、メイン スレッドで実行されるようです。

それ以外の場合、 の完了ブロックの実行コンテキストNSOperationは「未定義」です。つまり、完了ブロックは任意のスレッド/キューで実行できます。実際、それはいくつかのプライベート キューで実行されます。

IMO、実行コンテキストが呼び出しサイトによって明示的に指定されない限り、これは推奨されるアプローチです。インスタンスにアクセス可能なスレッドまたはキュー (メイン スレッドなど) で完了ハンドラーを実行すると、不注意な開発者が簡単にデッド ロックを引き起こす可能性があります。


編集:

親操作の完了ブロックが終了したに依存操作を開始する場合は、完了ブロックの内容自体を(新しい親) にして、この操作を依存関係として子操作に追加して開始することで解決できます。それをキューに入れます。ただし、これはすぐに扱いにくくなることに気付くかもしれません。NSBlockOperation

別のアプローチでは、より簡潔で簡単な方法で非同期の問題を解決するのに特に適したユーティリティ クラスまたはクラス ライブラリが必要になります。ReactiveCocoaは、このような (簡単な) 問題を解決することができます。ただし、これは過度に複雑であり、実際には「学習曲線」があり、急勾配です。学習に数週間を費やすことに同意し、他の多くの非同期ユースケースやさらに複雑なものを使用することに同意しない限り、お勧めしません。

より単純なアプローチは、JavaScript、Python、Scala、および他のいくつかの言語でかなり一般的な「Promises」を利用することです。

さて、よく読んでください。(簡単な)解決策は実際には以下のとおりです。

"約束" (Future または Deferred と呼ばれることもあります)は、非同期タスクの最終的な結果を表します。あなたのフェッチリクエストはそのような非同期タスクです。ただし、完了ハンドラーを指定する代わりに、非同期メソッド/タスクはPromiseを返します。

-(Promise*) fetchThingsWithURL:(NSURL*)url;

次のように、成功ハンドラー ブロックまたは失敗ハンドラー ブロックを登録して、結果 (またはエラー) を取得します。

Promise* thingsPromise = [self fetchThingsWithURL:url];
thingsPromise.then(successHandlerBlock, failureHandlerBlock);

または、インライン化されたブロック:

thingsPromise.then(^id(id things){
   // do something with things
   return <result of success handler>
}, ^id(NSError* error){
   // Ohps, error occurred
   return <result of failure handler>
});

そして短い:

[self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil);

ここでparseAsync:は、Promise を返す非同期メソッドを示します。(はい、約束です)。


パーサーから結果を取得する方法を疑問に思うかもしれません。

[self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil)
.then(^id(id parserResult){
    NSLog(@"Parser returned: %@", parserResult);
    return nil;  // result not used
}, nil);

これにより、実際に async task が開始されますfetchThingsWithURL:。次に、正常に終了すると、 async task が開始されますparseAsync:。次に、これが正常に終了すると結果が出力され、それ以外の場合はエラーが出力されます。

複数の非同期タスクを順番に次々と呼び出すことを、「継続」または「連鎖」と呼びます。

上記のステートメント全体が非同期であることに注意してください。つまり、上記のステートメントをメソッドにラップして実行すると、メソッドはすぐに戻ります。


失敗などのエラーをキャッチする方法を疑問に思うかもしれません。fetchThingsWithURL:parseAsync:

[self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil)
.then(^id(id parserResult){
    NSLog(@"Parser returned: %@", parserResult);
    return nil;  // result not used
}, nil)
.then(/*succes handler ignored*/, ^id (NSError* error){
    // catch any error
    NSLog(@"ERROR: %@", error);
    return nil; // result not used
});

ハンドラーは、対応するタスクが終了した後に実行されます (もちろん)。タスクが成功すると、成功ハンドラーが呼び出されます (存在する場合)。タスクが失敗した場合、エラー ハンドラが呼び出されます (存在する場合)。

ハンドラーPromise (またはその他のオブジェクト) を返す場合があります。たとえば、非同期タスクが正常に終了した場合、その成功ハンドラーが呼び出され、別の非同期タスクが開始され、promise が返されます。そして、これが終了したら、さらに別のものを開始することができます。それは「継続」です;)


ハンドラから何でも返すことができます:

Promise* finalResult = [self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil)
.then(^id(id parserResult){
    return @"OK";
}, ^id(NSError* error){
    return error;
});

ここで、finalResultは最終的に値 @"OK" または NSError になります。


最終的な結果を配列に保存できます。

array = @[
    [self task1],
    [self task2],
    [self task3]
];

すべてのタスクが正常に終了したら続行します。

[Promise all:array].then(^id(results){
    ...
}, ^id (NSError* error){
    ...
});

promise の値を設定することは、「解決」と呼ばれます。Promise は 1 回だけ解決できます。

完了ハンドラーまたは完了デリゲートを使用して、任意の非同期メソッドをプロミスを返すメソッドにラップできます。

- (Promise*) fetchUserWithURL:(NSURL*)url 
{
    Promise* promise = [Promise new];

    HTTPOperation* op = [[HTTPOperation alloc] initWithRequest:request 
        success:^(NSData* data){
            [promise fulfillWithValue:data];
        } 
        failure:^(NSError* error){
            [promise rejectWithReason:error];
        }];

    [op start];

    return promise;
}

タスクの完了時に、結果値を渡して約束を「履行」するか、理由 (エラー) を渡して「拒否」することができます。

実際の実装によっては、Promise をキャンセルすることもできます。たとえば、リクエスト操作への参照を保持しているとします。

self.fetchUserPromise = [self fetchUsersWithURL:url];

次のように非同期タスクをキャンセルできます。

- (void) viewWillDisappear:(BOOL)animate {
    [super viewWillDisappear:animate];
    [self.fetchUserPromise cancel];
    self.fetchUserPromise = nil;
}

関連する非同期タスクをキャンセルするには、ラッパーに失敗ハンドラーを登録します。

- (Promise*) fetchUserWithURL:(NSURL*)url 
{
    Promise* promise = [Promise new];

    HTTPOperation* op = ... 
    [op start];

    promise.then(nil, ^id(NSError* error){
        if (promise.isCancelled) {
            [op cancel];
        }
        return nil; // result unused
    });

    return promise;
}

注: 成功または失敗のハンドラーは、いつ、どこで、いくつでも登録できます。


このように、Promise を使用して多くのことができます。この簡単な紹介よりもさらに多くのことができます。ここまで読めば、実際の問題を解決する方法がわかるかもしれません。それはすぐそこにあります - それは数行のコードです。

Promise のこの短い紹介は非常に大雑把であり、Objective-C 開発者にとってもまったく新しいものであり、珍しいように聞こえるかもしれないことは認めます。

Promise については、JS コミュニティでたくさん読むことができます。Objective-C には 1 つまたは 3 つの実装があります。実際の実装は、数百行のコードを超えることはありません。たまたま、私はそのうちの1つの作者です:

RX約束

私はおそらく完全に偏見があり、他のすべての人も Promises を扱ったことがあるようです。;)

于 2013-09-11T17:23:51.820 に答える