34

Grand Central Dispatchを使用すると、非メイン スレッドで時間のかかるタスクを簡単に実行でき、メイン スレッドのブロックを回避し、UI の応答性を維持できます。dispatch_asyncグローバル同時実行キューを使用してタスクを実行するだけです。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // code
});

ただし、これには通常マイナス面があるように、何かがうますぎるように聞こえます。これを iOS アプリ プロジェクトで何度も使用した後、最近、64 スレッドの制限があることがわかりました。制限に達すると、アプリはフリーズまたはハングします。Xcode でアプリを一時停止すると、メイン スレッドが によって保持されていることがわかりますsemaphore_wait_trap

ウェブでグーグル検索すると、他の人もこの問題に遭遇していることが確認されていますが、これに対する解決策は今のところ見つかりません.

Dispatch Thread Hard Limit Reached: 64 (同期操作でブロックされたディスパッチ スレッドが多すぎます)

別のスタックオーバーフローの質問dispatch_syncでは、 and dispatch_barrier_asynctooを使用するとこの問題が発生することが確認されています。

質問:
Grand Central Dispatch には 64 スレッドの制限があるため、回避策はありますか?

前もって感謝します!

4

1 に答える 1

70

まあ、あなたが束縛され決心しているなら、GCD の束縛から解放され、pthreads を使用して OS のプロセスごとのスレッド制限に真っ向からぶつかることができますが、結論は次のとおりです。 GCD のキュー幅の制限を超えている場合は、同時実行アプローチの再評価を検討することをお勧めします。

極端な場合、制限に達する方法は 2 つあります。

  1. ブロッキング syscall を介して、一部の OS プリミティブで 64 のスレッドをブロックすることができます。(I/Oバウンド)
  2. 合法的に 64 の実行可能なタスクをすべて同時にロックする準備ができています。(CPUバウンド)

状況 #1 の場合、推奨されるアプローチは非ブロッキング I/O を使用することです。実際、GCD には 10.7/Lion IIRC で導入された多数の呼び出しがあり、I/O の非同期スケジューリングを容易にし、スレッドの再利用を改善します。GCD I/O メカニズムを使用する場合、これらのスレッドは I/O を待って拘束されることはありません。GCD は、ファイル記述子 (またはマッハ ポート) でデータが利用可能になると、ブロック (または関数) をキューに入れるだけです。dispatch_io_createand friendsのドキュメントを参照してください。

役立つ場合のために、GCD I/O メカニズムを使用して実装された TCP エコー サーバーの小さな例 (保証なしで提示) を次に示します。

in_port_t port = 10000;
void DieWithError(char *errorMessage);

// Returns a block you can call later to shut down the server -- caller owns block.
dispatch_block_t CreateCleanupBlockForLaunchedServer()
{
    // Create the socket
    int servSock = -1;
    if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        DieWithError("socket() failed");
    }

    // Bind the socket - if the port we want is in use, increment until we find one that isn't
    struct sockaddr_in echoServAddr;
    memset(&echoServAddr, 0, sizeof(echoServAddr));
    echoServAddr.sin_family = AF_INET;
    echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    do {
        printf("server attempting to bind to port %d\n", (int)port);
        echoServAddr.sin_port = htons(port);
    } while (bind(servSock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0 && ++port);

    // Make the socket non-blocking
    if (fcntl(servSock, F_SETFL, O_NONBLOCK) < 0) {
        shutdown(servSock, SHUT_RDWR);
        close(servSock);
        DieWithError("fcntl() failed");
    }

    // Set up the dispatch source that will alert us to new incoming connections
    dispatch_queue_t q = dispatch_queue_create("server_queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_source_t acceptSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, servSock, 0, q);
    dispatch_source_set_event_handler(acceptSource, ^{
        const unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
        for (unsigned long i = 0; i < numPendingConnections; i++) {
            int clntSock = -1;
            struct sockaddr_in echoClntAddr;
            unsigned int clntLen = sizeof(echoClntAddr);

            // Wait for a client to connect
            if ((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr, &clntLen)) >= 0)
            {
                printf("server sock: %d accepted\n", clntSock);

                dispatch_io_t channel = dispatch_io_create(DISPATCH_IO_STREAM, clntSock, q, ^(int error) {
                    if (error) {
                        fprintf(stderr, "Error: %s", strerror(error));
                    }
                    printf("server sock: %d closing\n", clntSock);
                    close(clntSock);
                });

                // Configure the channel...
                dispatch_io_set_low_water(channel, 1);
                dispatch_io_set_high_water(channel, SIZE_MAX);

                // Setup read handler
                dispatch_io_read(channel, 0, SIZE_MAX, q, ^(bool done, dispatch_data_t data, int error) {
                    BOOL close = NO;
                    if (error) {
                        fprintf(stderr, "Error: %s", strerror(error));
                        close = YES;
                    }

                    const size_t rxd = data ? dispatch_data_get_size(data) : 0;
                    if (rxd) {
                        // echo...
                        printf("server sock: %d received: %ld bytes\n", clntSock, (long)rxd);
                        // write it back out; echo!
                        dispatch_io_write(channel, 0, data, q, ^(bool done, dispatch_data_t data, int error) {});
                    }
                    else {
                        close = YES;
                    }

                    if (close) {
                        dispatch_io_close(channel, DISPATCH_IO_STOP);
                        dispatch_release(channel);
                    }
                });
            }
            else {
                printf("accept() failed;\n");
            }
        }
    });

    // Resume the source so we're ready to accept once we listen()
    dispatch_resume(acceptSource);

    // Listen() on the socket
    if (listen(servSock, SOMAXCONN) < 0) {
        shutdown(servSock, SHUT_RDWR);
        close(servSock);
        DieWithError("listen() failed");
    }

    // Make cleanup block for the server queue
    dispatch_block_t cleanupBlock = ^{
        dispatch_async(q, ^{
            shutdown(servSock, SHUT_RDWR);
            close(servSock);
            dispatch_release(acceptSource);
            dispatch_release(q);
        });
    };

    return Block_copy(cleanupBlock);
}

とにかく...当面のトピックに戻ります。

2 番目の状況にある場合は、「このアプローチによって本当に何かを得ているのか?」と自問する必要があります。12 コア、24 のハイパースレッディング/仮想コアを搭載した最も精巧な MacPro を持っているとしましょう。64 スレッドの場合、およそ 3:1 のスレッドと仮想コアの比率。コンテキストの切り替えとキャッシュ ミスは無料ではありません。このシナリオでは I/O バウンドではないと想定したので、コアよりも多くのタスクを実行することは、コンテキスト スイッチとキャッシュ スラッシュで CPU 時間を浪費することだけです。

実際には、キュー幅の制限に達したためにアプリケーションがハングしている場合、最も可能性の高いシナリオは、キューが不足していることです。デッドロックにつながる依存関係を作成した可能性があります。私が最も頻繁に見たケースは、dispatch_syncスレッドが残っていないときに、複数のインターロックされたスレッドが同じキューにアクセスしようとしている場合です。これは常に失敗します。

理由は次のとおりです。キュー幅は実装の詳細です。GCD の 64 スレッド幅の制限は文書化されていません。これは、適切に設計された同時実行アーキテクチャはキュー幅に依存してはならないためです。2 スレッド幅のキューが最終的に 1000 スレッド幅のキューと同じ結果 (遅い場合) になるように、同時実行アーキテクチャを常に設計する必要があります。そうしないと、キューが不足する可能性が常にあります。ワークロードを並列化可能なユニットに分割することで、基本的な機能の要件ではなく、最適化の可能性が開かれます。開発中にこの規律を強制する 1 つの方法は、並行キューを使用する場所でシリアル キューを使用してみることですが、連動しない動作が予想されます。

また、元の質問の正確なポイント: IIUC、64 スレッドの制限はトップレベルの同時キューあたり64 スレッドであるため、本当に必要だと感じた場合は、3 つのトップレベルの同時キュー (デフォルト、高、低) をすべて使用できます。優先度) 合計 64 を超えるスレッドを達成します。ただし、これを行わないでください。代わりに、それ自体が枯渇しないように設計を修正してください。あなたはもっと幸せになるでしょう。とにかく、上で示唆したように、64 スレッド幅のキューが不足している場合、おそらく最終的には 3 つの最上位レベルのキューすべてがいっぱいになるか、プロセスごとのスレッド制限に達して、そのようにして自分自身を飢えさせることになります。

于 2013-03-01T04:57:26.510 に答える