web-dev-qa-db-ja.com

メインキューのdispatch_syncとdispatch_async

私と一緒に耐えて、これはいくつかの説明が必要です。以下のような関数があります。

コンテキスト:「aProject」は、LPFileという別のコアデータエンティティのインスタンスを含む「memberFiles」という名前の配列を持つ、LPProjectという名前のコアデータエンティティです。各LPFileはディスク上のファイルを表し、各ファイルを開いてテキストを解析し、OTHERファイルを指す@importステートメントを探します。 @importステートメントが見つかった場合、それらが指すファイルを見つけ、最初のファイルを表すコアデータエンティティに関係を追加して、このファイルをこのファイルに「リンク」します。大きなファイルではすべての処理に時間がかかる可能性があるため、GCDを使用してメインスレッドから実行します。

_- (void) establishImportLinksForFilesInProject:(LPProject *)aProject {
    dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     for (LPFile *fileToCheck in aProject.memberFiles) {
         if (//Some condition is met) {
            dispatch_async(taskQ, ^{
                // Here, we do the scanning for @import statements. 
                // When we find a valid one, we put the whole path to the imported file into an array called 'verifiedImports'. 

                // go back to the main thread and update the model (Core Data is not thread-safe.)
                dispatch_sync(dispatch_get_main_queue(), ^{

                    NSLog(@"Got to main thread.");

                    for (NSString *import in verifiedImports) {  
                            // Add the relationship to Core Data LPFile entity.
                    }
                });//end block
            });//end block
        }
    }
}
_

さて、ここで物事が奇妙になります:

このコードは機能しますが、奇妙な問題が発生しています。いくつかのファイル(約20)があるLPProjectで実行すると、完全に実行されます。ただし、より多くのファイル(たとえば60-70)があるLPProjectで実行すると、[〜#〜] not [〜#〜]が正しく実行されます。メインスレッドに戻ることはなく、NSLog(@"got to main thread");は表示されず、アプリはハングします。しかし、(そしてこれは本当に物事が奇妙になる場所です)---最初に小さなプロジェクトでコードを実行し、次に大きなプロジェクトでそれを実行すると、すべてが完全に動作します。問題が発生するのは、最初に大規模プロジェクトでコードを実行したときだけです。

そして、2番目のディスパッチ行をこれに変更した場合のキッカーです。

_dispatch_async(dispatch_get_main_queue(), ^{
_

(つまり、asyncの代わりにsyncを使用して、ブロックをメインキューにディスパッチします)、すべてが常に機能します。完全に。プロジェクト内のファイルの数に関係なく!

この動作を説明するのに途方に暮れています。次にテストするものに関するヘルプやヒントをいただければ幸いです。

52
Bryan

これは、ディスクI/OおよびGCDに関連する一般的な問題です。基本的に、GCDはおそらくファイルごとに1つのスレッドを生成し、特定の時点でシステムが適切な時間内にサービスするにはスレッドが多すぎます。

Dispatch_async()を呼び出し、そのブロックで任意のI/Oを試みるたびに(たとえば、ここでいくつかのファイルを読んでいるように見えます)、そのコードブロックが実行されているスレッドがブロックする可能性があります(OSによって一時停止されます)、データがファイルシステムから読み取られるのを待機します。 GCDの動作方法は、ワーカースレッドの1つがI/Oでブロックされていることを確認し、さらに多くの作業を並行して行うように求めている場合、新しいワーカースレッドを生成するだけです。したがって、並行キューで50個のファイルを開こうとすると、GCDが最大50個のスレッドを生成することになります。

これは、システムが意味のあるサービスを提供するにはスレッドが多すぎるため、メインスレッドのCPUが不足することになります。

これを修正する方法は、並行キューの代わりにシリアルキューを使用してファイルベースの操作を行うことです。簡単です。シリアルキューを作成し、オブジェクトにivarとして保存すると、複数のシリアルキューが作成されないようになります。したがって、この呼び出しを削除します。

dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

これをinitメソッドに追加します。

taskQ = dispatch_queue_create("com.yourcompany.yourMeaningfulLabel", DISPATCH_QUEUE_SERIAL);

これをdeallocメソッドに追加します。

dispatch_release(taskQ);

そして、これをクラス宣言のivarとして追加します。

dispatch_queue_t taskQ;

53
Ryan

ライアンは正しい道を進んでいると思います。プロジェクトに1,500個のファイル(テストすることにした量)がある場合、スポーンされるスレッドが多すぎます。

そこで、上記のコードを次のようにリファクタリングしました。

- (void) establishImportLinksForFilesInProject:(LPProject *)aProject
{
        dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

     dispatch_async(taskQ, 
     ^{

     // Create a new Core Data Context on this thread using the same persistent data store    
     // as the main thread. Pass the objectID of aProject to access the managedObject
     // for that project on this thread's context:

     NSManagedObjectID *projectID = [aProject objectID];

     for (LPFile *fileToCheck in [backgroundContext objectWithID:projectID] memberFiles])
     {
        if (//Some condition is met)
        {
                // Here, we do the scanning for @import statements. 
                // When we find a valid one, we put the whole path to the 
                // imported file into an array called 'verifiedImports'. 

                // Pass this ID to main thread in dispatch call below to access the same
                // file in the main thread's context
                NSManagedObjectID *fileID = [fileToCheck objectID];


                // go back to the main thread and update the model 
                // (Core Data is not thread-safe.)
                dispatch_async(dispatch_get_main_queue(), 
                ^{
                    for (NSString *import in verifiedImports)
                    {  
                       LPFile *targetFile = [mainContext objectWithID:fileID];
                       // Add the relationship to targetFile. 
                    }
                 });//end block
         }
    }
    // Easy way to tell when we're done processing all files.
    // Could add a dispatch_async(main_queue) call here to do something like UI updates, etc

    });//end block
    }

したがって、基本的には、ファイルごとに1つのスレッドではなく、すべてのファイルを読み取る1つのスレッドを生成しています。また、main_queueでdispatch_async()を呼び出すのが正しいアプローチであることがわかります。ワーカースレッドは、そのブロックをメインスレッドにディスパッチし、次のファイルのスキャンに進む前にブロックが戻るのを待機しません。

この実装は、Ryanが示唆したように「シリアル」キューを基本的に設定します(forループはそのシリアル部分です)が、1つの利点があります。forループが終了すると、すべてのファイルの処理が完了し、 dispatch_async(main_queue)ブロックを使用して、必要な処理を実行します。これは、並行処理タスクがいつ完了したかを知るための非常に良い方法であり、古いバージョンには存在しませんでした。

ここでの欠点は、複数のスレッドでCore Dataを操作するのが少し複雑になることです。しかし、このアプローチは、5,000ファイル(これは私がテストした中で最高です)を含むプロジェクトでは防弾と思われます。

5
Bryan

ダイアグラムを使用すると理解しやすいと思います。

著者が説明した状況について:

| taskQ | ***********開始|

| dispatch_1 *********** | ---------

| dispatch_2 ************* | ---------

| dispatch_n *************************** | ----------

| main queue(sync)| ** mainへのディスパッチを開始|

************************* | --dispatch_1-- | --dispatch_2-- | --dispatch3-- | ****** *********************** | --dispatch_n |、

同期メインキューが非常にビジーになり、最終的にタスクが失敗します。

0
Damon Yuan