web-dev-qa-db-ja.com

スレッドプールを使用したstd :: asyncのVisualC ++実装は合法ですか

Visual C++は、_std::async_を_std::launch::async_で呼び出すときに、Windowsスレッドプール(使用可能な場合はVistaのCreateThreadpoolWork、使用できない場合はQueueUserWorkItem)を使用します。

プール内のスレッドの数は制限されています。スリープせずに長時間実行される(I/Oの実行を含む)複数のタスクを作成すると、キュー内の次のタスクが機能する機会がなくなります。

標準(私はN4140を使用しています)は、_std::async_を_std::launch::async_と一緒に使用すると述べています

...呼び出しINVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)(20.9.2、30.3.1.2)スレッドオブジェクトで表される新しい実行スレッドのようにDECAY_COPY()の呼び出しはasyncを呼び出したスレッドで評価されます。

(§30.6.8p3、強調鉱山。)

_std::thread_のコンストラクターは新しいスレッドなどを作成します。

一般的なスレッドについては(§1.10p3):

実装では、ブロックされていないすべてのスレッドが最終的に進行するようにする必要があります。 [注:標準ライブラリ関数は、I/Oまたはロックをサイレントにブロックする場合があります。外部から課せられたスレッドの優先順位を含む実行環境の要因により、実装が前進を確実に保証できない場合があります。 —エンドノート]

一連のOSスレッドまたは_std::thread_ sを作成し、それらすべてが非常に長い(おそらく無限の)タスクを実行すると、それらはすべてスケジュールされます(少なくとも、Windowsでは、優先順位やアフィニティなどをいじることなく)。同じタスクをWindowsスレッドプールにスケジュールする場合(またはstd::async(std::launch::async, ...)を使用してスケジュールする場合)、後でスケジュールされたタスクは、前のタスクが終了するまで実行されません。

これは厳密に言えば合法ですか?そして、「最終的に」とはどういう意味ですか?


問題は、最初にスケジュールされたタスクが事実上無限である場合、残りのタスクが実行されないことです。そのため、他のスレッド(OSスレッドではなく、as-ifルールによる「C++スレッド」)は進行しません。

コードに無限ループがある場合、動作は未定義であり、したがって合法であると主張する人もいるかもしれません。

しかし、私は、標準がUBにそれを起こさせると言っている問題のある種類の無限ループは必要ないと主張します。揮発性オブジェクトへのアクセス、アトミック操作の実行、および同期操作はすべて、ループの終了に関する仮定を「無効にする」副作用です。

(次のラムダを実行する非同期呼び出しがたくさんあります

_auto lambda = [&] {
    while (m.try_lock() == false) {
        for (size_t i = 0; i < (2 << 24); i++) {
            vi++;
        }
        vi = 0;
    }
};
_

ロックはユーザー入力時にのみ解放されます。しかし、他にも有効な種類の正当な無限ループがあります。)

このようなタスクをいくつかスケジュールすると、後でスケジュールしたタスクが実行されなくなります。

本当に邪悪な例は、ロックが解除される/フラグが立てられるまで実行されるタスクが多すぎることです。その後、 `std :: async(std :: launch :: async、...)を使用してフラグを立てるタスクをスケジュールします。 。 「最終的に」という言葉が非常に驚くべきことを意味しない限り、このプログラムは終了しなければなりません。しかし、VC++の実装では、そうではありません。

私にはそれは基準違反のように思えます。私が不思議に思うのは、メモの2番目の文です。要因により、実装が前進を確実に保証できない場合があります。では、これらの実装はどのように準拠していますか?

これは、実装がメモリオーダリング、アトミック性、さらには実行の複数のスレッドの存在の特定の側面を提供することを妨げる要因があるかもしれないと言っているようなものです。すばらしいですが、準拠するホストされた実装は複数のスレッドをサポートする必要があります。彼らと彼らの要因にとってはあまりにも悪い。それらを提供できない場合、それはC++ではありません。

これは要件の緩和ですか?そのように解釈する場合、それは要素が何であるか、そしてさらに重要なことに、どの保証が実装によって提供されない可能性があるかを指定しないため、要件の完全な撤回です。

そうでない場合-そのメモはどういう意味ですか?

ISO/IEC指令によると、脚注が非規範的であったことを思い出しますが、脚注についてはよくわかりません。私はISO/IEC指令で次のことを見つけました:

24ノート

24.1目的または理論的根拠

メモは、ドキュメントのテキストの理解または使用を支援することを目的とした追加情報を提供するために使用されます。 文書は注記なしで使用できるものとします。

強調鉱山。その不明確な注記のないドキュメントを検討すると、スレッドは進歩しなければならないように思えます。std::async(std::launch::async, ...)には効果がありますas-ifファンクターは新しいスレッドで実行されます。 _std::thread_を使用して作成されていたため、std::async(std::launch::async, ...)を使用してディスパッチされたファンクターは進行する必要があります。また、スレッドプールを使用したVC++の実装では、そうではありません。したがって、VC++はこの点で標準に違反しています。


完全な例、i5-6440HQ上のWindows 10 Enterprise1607でVS2015U3を使用してテスト:

_#include <iostream>
#include <future>
#include <atomic>

int main() {
    volatile int vi{};
    std::mutex m{};
    m.lock();

    auto lambda = [&] {
        while (m.try_lock() == false) {
            for (size_t i = 0; i < (2 << 10); i++) {
                vi++;
            }
            vi = 0;
        }
        m.unlock();
    };

    std::vector<decltype(std::async(std::launch::async, lambda))> v;

    int threadCount{};
    std::cin >> threadCount;
    for (int i = 0; i < threadCount; i++) {
        v.emplace_back(std::move(std::async(std::launch::async, lambda)));
    }

    auto release = std::async(std::launch::async, [&] {
        __asm int 3;
        std::cout << "foo" << std::endl;
        vi = 123;
        m.unlock();
    });

    return 0;
}
_

4以下で終了します。 4を超えると、そうではありません。


同様の質問:

27
conio

状況はC++ 17で P0296R2 によっていくらか明らかにされています。 Visual C++の実装で、スレッドが同時進行の保証を提供しない(一般的には望ましくない)と文書化されていない限り、制限付きスレッドプールは準拠していません( C++ 17で)。

「外部から課せられたスレッドの優先順位」に関する注記は削除されました。おそらく、環境がC++プログラムの進行を防ぐことがすでに常に可能であるためです(優先順位がない場合は一時停止され、そうでない場合は電源によって)またはハードウェア障害)。

そのセクションには残りの規範的な「すべき」が1つありますが、それは(conio 言及 として)ロックフリー操作にのみ関係し、同じスレッドへの他のスレッドによる頻繁な同時アクセスによって無期限に遅延する可能性がありますキャッシュライン(同じアトミック変数だけではありません)。 (一部の実装では、他のスレッドが読み取りのみを行っている場合でも、これが発生する可能性があると思います。)

3
Davis Herring