28 votes

Est Visual C++ mise en œuvre de std::asynchrone à l'aide d'un pool de threads juridique

Visual C++ utilise le pool de threads Windows (Vista CreateThreadpoolWork , si disponibles, et QueueUserWorkItem si non) lors de l'appel d' std::async avec std::launch::async.

Le nombre de threads dans la piscine est limitée. Si créer plusieurs tâches qui s'exécutent pendant une longue période sans sommeil (y compris I/O), les tâches à venir dans la file d'attente n'obtiendrez pas une chance de travailler.

La norme (je suis en utilisant N4140) dit que l'utilisation d' std::async avec std::launch::async

... des appels INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...) (20.9.2, 30.3.1.2) comme si dans un nouveau thread d'exécution représentée par un objet thread avec les appels à l' DECAY_COPY() cours d'évaluation dans le thread qui appelle async.

(§30.6.8p3, c'est moi qui souligne.)

std::threads'constructeur crée un nouveau thread, etc.

À propos des threads en général, il est dit (§1.10p3):

Implémentations devraient veiller à ce que tous débloqué threads, éventuellement, de faire des progrès. [Remarque: les fonctions de bibliothèque Standard peuvent bloquer silencieusement sur les I/O ou des serrures. Les facteurs dans l'environnement d'exécution, y compris imposés de l'extérieur les priorités des threads, ce qui peut empêcher une mise en œuvre de la fabrication de certaines garanties de la progression. -la note de fin]

Si je crée un tas d'OS de threads ou de l' std::threads, tout en effectuant quelques très longtemps (peut-être infini) des tâches, ils vont tous être prévue (au moins sur Windows; sans vous embêter avec des priorités, des affinités, etc.). Si nous programmons les mêmes tâches pour le pool de threads Windows (ou utilisez std::async(std::launch::async, ...) qui le fait), plus tard les tâches planifiées ne fonctionnera pas jusqu'à ce que le plus tôt les tâches de finition.

Est-ce légal, à proprement parler? Et ce n'est "finalement" signifie?


Le problème est que si les tâches planifiées premiers sont de facto à l'infini, le reste des tâches ne fonctionne pas. Donc, les autres threads (pas OS fils, mais le "C++-fils", selon le comme-si la règle) ne ferez pas de progrès.

On peut arguer que si le code a une boucle infini le comportement est indéfini, et donc c'est légal.

Mais je soutiens que nous n'avons pas besoin d'une boucle infinie de la problématique genre que dit la norme causes UB pour que cela se produise. L'accès volatils objets, l'exécution d'opération atomique et les opérations de synchronisation sont tous les effets secondaires que l'option "désactiver" l'hypothèse sur les boucles de mettre fin.

(J'ai un tas d'appels asynchrones de l'exécution de la suite de lambda

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

et le verrou est relâché lors de la saisie de l'utilisateur. Mais il existe d'autres types de légitime les boucles infinies.)

Si je programme un couple de ces tâches, les tâches de l'annexe I après eux de ne pas arriver à courir.

Un vraiment méchants exemple serait le lancement de trop nombreuses tâches qui s'exécutent jusqu'à ce qu'une serrure de presse/un drapeau est hissé, puis planifier à l'aide de `std::async(std::lancement::asynchrone, ...) une tâche qui soulève le drapeau. À moins que le mot "éventuellement" signifie quelque chose de très surprenant, ce programme a pour résilier. Mais sous VC++ mise en œuvre, il ne le fera pas!

Pour moi, il semble comme une violation de la norme. Ce qui me fait me demander agit de la deuxième phrase de la note. Facteurs peuvent empêcher les implémentations de donner certaines garanties de la progression. Alors, comment sont ces mise en œuvre conforme?

C'est comme dire il peut y avoir des facteurs qui empêchent les implémentations de fournir certains aspect de la mémoire de la commande, l'atomicité, ou même de l'existence de plusieurs threads d'exécution. Grande, mais conformes hébergé implémentations doivent supporter plusieurs threads. Tant pis pour eux et leurs facteurs. Si elles ne peuvent pas eux c'est pas du C++.

Est-ce un assouplissement de l'exigence? Si l'interprétation de la sorte, c'est un retrait complet de l'exigence, car il ne précise pas quels sont les facteurs et, plus important encore, ce qui garantit peut-être pas fourni par les implémentations.

Si ce n'est pas ce que remarque même dire?

Je me souviens de notes de bas de page non-normatifs selon les Directives ISO/CEI, mais je ne suis pas sûr à propos de notes. J'ai trouvé dans les directives ISO/CEI suivantes:

24 Notes

24.1 But ou la raison d'être

Les Notes sont utilisés pour donner des informations supplémentaires destinées à faciliter la compréhension ou de l'utilisation du texte du document. Le document doit être utilisable sans les notes.

C'est moi qui souligne. Si je considère que le document sans que sait pas remarque, me semble threads doivent faire des progrès, std::async(std::launch::async, ...) a pour effet -comme si le foncteur est d'exécuter sur un nouveau fil de discussion, comme si elle avait été créée à l'aide de std::thread, et donc un foncteurs distribué à l'aide de std::async(std::launch::async, ...) doit faire des progrès. Et dans le VC++ mise en œuvre avec le pool de threads ils ne le font pas. Donc, VC++ est en violation de la norme à cet égard.


Exemple complet, testé à l'aide de VS 2015U3 sur Windows 10 Enterprise 1607 sur i5-6440HQ:

#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;
}

Avec 4 ou moins, il se termine. Avec plus de 4, il ne le fait pas.


Des questions similaires:

5voto

Davis Herring Points 7254

La situation a été précisé dans C++17 par P0296R2. À moins que le Visual C++ documents de mise en œuvre que son fils ne pas fournir simultanées de la progression des garanties (ce qui serait généralement pas souhaitable), la délimitée pool de threads n'est pas conforme (en C++17).

La remarque à propos de "imposée de l'extérieur, les priorités des threads" a été supprimé, peut-être parce qu'il est toujours possible pour l'environnement afin de prévenir l'évolution d'un programme C++ (si ce n'est par ordre de priorité, puis en étant suspendu, et si ce n'est qu', puis par la puissance ou de défaillance du matériel).

Il reste un normative "devrait" dans cette section, mais il se rapporte (comme conio mentionné) seulement sans verrouillage des opérations, qui peut être retardé indéfiniment par de fréquents accès simultané par un autre fil sur le même ligne de cache (pas simplement la même variable atomique). (Je pense que dans certaines implémentations cela peut se produire même si les autres threads ne sont que de la lecture.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X