4

たとえば、次のように、「本物の」非同期メソッドをシリアル化できるようにしたかったのです。

  • Web リクエストの作成
  • UIAlertView を表示する

これは通常、扱いにくいビジネスであり、シリアル キューのほとんどのサンプルは、NSBlockOperation のブロックで「スリープ」を示しています。コールバックが発生したときにのみ操作が完了するため、これは機能しません。

NSOperation をサブクラス化することでこれを実装してみました。実装の最も興味深い部分は次のとおりです。

+ (MYOperation *)operationWithBlock:(CompleteBlock)block
{
    MYOperation *operation = [[MYOperation alloc] init];
    operation.block = block;
    return operation;
}

- (void)start
{
    [self willChangeValueForKey:@"isExecuting"];
    self.executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
    if (self.block) {
        self.block(self);
    }
}

- (void)finish
{
    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];
    self.executing = NO;
    self.finished = YES;
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isFinished
{
    return self.finished;
}

- (BOOL) isExecuting
{
    return self.executing;
}

これはうまく機能します。ここにデモンストレーションがあります...

NSOperationQueue *q = [[NSOperationQueue alloc] init];
q.maxConcurrentOperationCount = 1;

dispatch_queue_t queue = dispatch_queue_create("1", NULL);
dispatch_queue_t queue2 = dispatch_queue_create("2", NULL);

MYOperation *op = [MYOperation operationWithBlock:^(MYOperation *o) {
    NSLog(@"1...");
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1");
        [o finish]; // this signals we're done
    });
}];

MYOperation *op2 = [MYOperation operationWithBlock:^(MYOperation *o) {
    NSLog(@"2...");
    dispatch_async(queue2, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2");
        [o finish]; // this signals we're done
    });
}];

[q addOperations:@[op, op2] waitUntilFinished:YES];

[NSThread sleepForTimeInterval:5];

スリープも使用しましたが、ネットワーク呼び出しをシミュレートするために、これらがバックグラウンド スレッドで実行されていることを確認しました。ログは次のようになります

1...
1
2...
2

これは希望どおりです。このアプローチの何が問題なのですか? 知っておくべき注意事項はありますか?

4

3 に答える 3

7

「シリアル化」非同期タスクは、実際には「継続」と呼ばれます (この wiki 記事Continuationも参照してください。

タスクを、非同期タスクの最終的な結果をパラメーターとする完了ハンドラーを持つ非同期関数/メソッドとして定義できるとします。たとえば、次のようになります。

typedef void(^completion_handler_t)(id result);

-(void) webRequestWithCompletion:(completion_handler_t)completionHandler;
-(void) showAlertViewWithResult:(id)result completion:(completion_handler_t)completionHandler;

ブロックを利用できるようにすると、前のタスクの完了ブロック内から次の非同期タスクを呼び出すことで、「継続」を簡単に実現できます。

- (void) foo 
{
    [self webRequestWithCompletion:^(id result) {  
        [self showAlertViewWithResult:result completion:^(id userAnswer) {
            NSLog(@"User answered with: %@", userAnswer);
        }
    }
}

fooメソッドは「非同期」に感染することに注意してください;)

つまり、ここでメソッドの最終的な効果foo、つまりユーザーの回答をコンソールに出力することは、実際には再び非同期です。

ただし、複数の非同期タスクを「連鎖」する、つまり、複数の非同期タスクを「継続」すると、すぐに扱いにくくなる可能性があります。

完了ブロックで「継続」を実装すると、各タスクの完了ハンドラーのインデントが増加します。さらに、ユーザーが任意の状態でタスクをキャンセルできるようにする手段を実装し、エラー状態を処理するコードを実装すると、コードはすぐに混乱し、書きにくく、理解しにくくなります。

「継続」、およびキャンセルとエラー処理を実装するためのより良いアプローチは、Futures または Promisesの概念を使用することです。Future または Promiseは、非同期タスクの最終的な結果を表します。基本的に、これは呼び出しサイトに「最終的な結果を通知する」ための別のアプローチです。

Objective-C では、「Promise」を通常のクラスとして実装できます。「Promise」を実装するサードパーティのライブラリがあります。次のコードは、特定の実装RXPromiseを使用しています。

このようなPromiseを利用する場合、次のようにタスクを定義します。

-(Promise*) webRequestWithCompletion;
-(Promise*) showAlertViewWithResult:(id)result;

注: 完了ハンドラはありません。

Promiseを使用すると、非同期タスクの「結果」はthen、promise のプロパティに「登録」される「成功」または「エラー」ハンドラーを介して取得されます。タスクが完了すると、成功ハンドラまたはエラー ハンドラがタスクによって呼び出されます。タスクが正常に終了すると、成功ハンドラが呼び出され、その結果が成功ハンドラのパラメータresultに渡されます。それ以外の場合、タスクが失敗すると、その理由がエラー ハンドラ (通常はNSErrorオブジェクト) に渡されます。

Promise の基本的な使用方法は次のとおりです。

Promise* promise = [self asyncTasks];
// register handler blocks with "then":
Promise* handlerPromise = promise.then( <success handler block>, <error handler block> );

成功ハンドラー ブロックには、typeのパラメーターresultidがあります。エラー ハンドラ ブロックには、 type のパラメータがありますNSError

このステートメントpromise.then(...)は、いずれかのハンドラーの結果を表す promise を返すことに注意してください。これは、「親」の promise が成功またはエラーで解決されたときに呼び出されます。ハンドラーの戻り値は、「即時の結果」(オブジェクト) または「最終的な結果」のいずれかであり、Promise オブジェクトとして表されます。

OP の問題のコメント付きサンプルを次のコード スニペットに示します (洗練されたエラー処理を含む)。

- (void) foo 
{
    [self webRequestWithCompletion] // returns a "Promise" object which has a property "then"
    // when the task finished, then:
    .then(^id(id result) {
        // on succeess:
        // param "result" is the result of method "webRequestWithCompletion"
        return [self showAlertViewWithResult:result];  // note: returns a promise
    }, nil /*error handler not defined, fall through to the next defined error handler */ )       
    // when either of the previous handler finished, then:
    .then(^id(id userAnswer) {
        NSLog(@"User answered with: %@", userAnswer);
        return nil;  // handler's result not used, thus nil.
    }, nil)
    // when either of the previous handler finished, then:
    .then(nil /*success handler not defined*/, 
    ^id(NEError* error) {
         // on error
         // Error handler. Last error handler catches all errors.
         // That is, either a web request error or perhaps the user cancelled (which results in rejecting the promise with a "User Cancelled" error)
         return nil;  // result of this error handler not used anywhere.
    });

}

コードには確かにもっと説明が必要です。詳細でより包括的な説明、および任意の時点でキャンセルを実行する方法については、「Promise」を実装する Objective-C クラスであるRXPromiseライブラリを参照してください。開示: 私は RXPromise ライブラリの作成者です。

于 2013-08-18T10:27:54.023 に答える
1

一見、これは機能しますが、「適切な」 NSOperation サブクラスを持つにはいくつかの部分が欠けています。

'cancelled' 状態に対応していません。開始をチェックインisCancelledし、これが YES を返した場合は開始しないでください ( 「キャンセル コマンドに応答しています」 ) 。

また、isConcurrentメソッドもオーバーライドする必要がありますが、簡潔にするために省略した可能性があります。

于 2013-08-13T07:32:12.063 に答える
0

NSOperation をサブクラス化するときは、スレッド セーフを台無しにするのは本当に簡単なので、自分が何をしているのか本当によくわかっていない限り、main のみをオーバーライドすることを強くお勧めします。ドキュメントには、操作は同時実行されないと記載されていますが、NSOperationQueue を介して実行すると、別のスレッドで実行することで自動的に同時実行されます。非並行性に関する注意事項は、NSOperation の start メソッドを自分で呼び出した場合にのみ適用されます。これは、NSLog の各行に含まれるスレッド ID を確認することで確認できます。例えば:

2013-09-17 22:49:07.779 AppNameGoesHere[58156:ThreadIDGoesHere] ログ メッセージがここに表示されます。

main をオーバーライドする利点は、操作の状態を変更するときにスレッド セーフに対処する必要がないことを意味します。NSOperation がそのすべてを処理します。コードをシリアル化する主なものは、maxConcurrentOperationCount を 1 に設定する行です。これは、キュー内の各操作が次に実行されるまで待機することを意味します (すべての操作は、NSOperationQueue によって決定されるランダム スレッドで実行されます)。各操作内で dispatch_async を呼び出す行為も、さらに別のスレッドをトリガーします。

サブクラス化 NSOperation を使用することに固執している場合は、メインのみをオーバーライドします。それ以外の場合は、ここで多少複製しているように見える NSBlockOperation を使用することをお勧めします。私は NSOperation を完全に避けたいと思っていますが、API は古くなり始めており、間違いを犯しやすいです。別の方法として、 RXPromiseまたはこの問題を解決するための私自身の試みFranticApparatusのようなものを提案します。

于 2013-09-18T04:13:23.547 に答える