web-dev-qa-db-fra.com

Comportement d'attente de thread C ++ 11: std :: this_thread :: yield () vs. std :: this_thread :: sleep_for (std :: chrono :: milliseconds (1))

On m'a dit lors de l'écriture de code C++ spécifique à Microsoft que l'écriture Sleep(1) est bien meilleure que Sleep(0) pour le verrouillage automatique, car Sleep(0) utilisera plus de temps CPU, de plus, il ne donne que s'il y a un autre thread de priorité égale en attente d'exécution.

Cependant, avec la bibliothèque de threads C++ 11, il n'y a pas beaucoup de documentation (au moins que j'ai pu trouver) sur les effets de std::this_thread::yield() vs std::this_thread::sleep_for( std::chrono::milliseconds(1) ); le second est certainement plus verbeux, mais sont-ils tous deux aussi efficaces pour un spinlock, ou souffrent-ils potentiellement des mêmes accrochages qui ont affecté Sleep(0) vs Sleep(1)?

Un exemple de boucle où std::this_thread::yield() ou std::this_thread::sleep_for( std::chrono::milliseconds(1) ) serait acceptable:

void SpinLock( const bool& bSomeCondition )
{
    // Wait for some condition to be satisfied
    while( !bSomeCondition )
    {
         /*Either std::this_thread::yield() or 
           std::this_thread::sleep_for( std::chrono::milliseconds(1) ) 
           is acceptable here.*/
    }

    // Do something!
}
36
Thomas Russell

La norme est quelque peu floue ici, car une implémentation concrète sera largement influencée par les capacités de planification du système d'exploitation sous-jacent.

Cela étant dit, vous pouvez assumer certaines choses en toute sécurité sur n'importe quel système d'exploitation moderne:

  • yield abandonnera la tranche de temps actuelle et réinsérera le thread dans la file d'attente de planification. La durée qui expire jusqu'à ce que le thread soit exécuté à nouveau dépend généralement entièrement du planificateur. Notez que la norme parle de rendement comme possibilité de rééchelonnement. Ainsi, une implémentation est entièrement libre de revenir d'un rendement immédiatement si elle le souhaite. Un rendement ne marquera jamais un fil comme inactif, donc un fil qui tourne sur un rendement produira toujours une charge de 100% sur un noyau. Si aucun autre thread n'est prêt, vous risquez de perdre au plus le reste de la tranche de temps actuelle avant d'être à nouveau planifié.
  • sleep_* Bloquera le fil pendant au moins la durée demandée. Une implémentation peut transformer une sleep_for(0) en yield. La fonction sleep_for(1), d'autre part, mettra votre thread en suspension. Au lieu de revenir à la file d'attente de planification, le thread va d'abord à une autre file d'attente de threads en veille. Ce n'est qu'après le délai demandé que le planificateur envisagera de réinsérer le thread dans la file d'attente de planification. La charge produite par un petit sommeil sera toujours très élevée. Si le temps de sommeil demandé est inférieur à une tranche de temps système, vous pouvez vous attendre à ce que le thread ne saute qu'une seule tranche de temps (c'est-à-dire un rendement pour libérer la tranche de temps active et ensuite sauter celle-ci par la suite), ce qui entraînera toujours une charge de processeur. proche ou même égal à 100% sur un cœur.

Quelques mots sur ce qui est mieux pour le verrouillage par rotation. Le verrouillage par rotation est un outil de choix lorsque l'on s'attend à peu ou pas de conflits sur le verrou. Si, dans la grande majorité des cas, vous vous attendez à ce que le verrou soit disponible, les verrous tournants sont une solution économique et précieuse. Cependant, dès que vous avez un conflit, les verrous tournants seront vous coûtent. Si vous vous demandez si le rendement ou le sommeil est la meilleure solution, les verrous tournants sont le mauvais outil pour le travail. Vous devriez plutôt utiliser un mutex.

Pour un verrou tournant, le cas où vous devez réellement attendre le verrou doit être considéré comme exceptionnel. Par conséquent, il est tout à fait correct de simplement céder ici - il exprime clairement l'intention et la perte de temps CPU ne devrait jamais être un problème en premier lieu.

32
ComicSansMS

Je viens de faire un test avec Visual Studio 2013 sur Windows 7, Intel i7 2,8 GHz, optimisations du mode de sortie par défaut.

sleep_for (non nul) semble dormir pendant un minimum d'environ une milliseconde et ne prend aucune ressource CPU dans une boucle comme:

for (int k = 0; k < 1000; ++k)
    std::this_thread::sleep_for(std::chrono::nanoseconds(1));

Cette boucle de 1 000 sommeil prend environ 1 seconde si vous utilisez 1 nanoseconde, 1 microseconde ou 1 milliseconde. D'un autre côté, yield () prend environ 0,25 microsecondes chacun mais fera tourner le CPU à 100% pour le thread:

for (int k = 0; k < 4,000,000; ++k) (commas added for clarity)
    std::this_thread::yield();

std :: this_thread :: sleep_for ((std :: chrono :: nanoseconds (0)) semble être à peu près la même chose que yield () (test non présenté ici).

En comparaison, le verrouillage d'un atomic_flag pour un spinlock prend environ 5 nanosecondes. Cette boucle dure 1 seconde:

std::atomic_flag f = ATOMIC_FLAG_INIT;
for (int k = 0; k < 200,000,000; ++k)
    f.test_and_set();

De plus, un mutex prend environ 50 nanosecondes, 1 seconde pour cette boucle:

for (int k = 0; k < 20,000,000; ++k)
    std::lock_guard<std::mutex> lock(g_mutex);

Sur cette base, je n'hésiterais probablement pas à mettre un rendement dans le spinlock, mais je n'utiliserais presque certainement pas sleep_for. Si vous pensez que vos verrous tourneront beaucoup et que la consommation de CPU vous inquiète, je passerais à std :: mutex si c'est pratique dans votre application. Espérons que les jours de très mauvaises performances sur std :: mutex sous Windows soient derrière nous.

14
Rob L

si vous êtes intéressé par la charge du processeur lors de l'utilisation de yield - c'est très mauvais, sauf un cas - (seule votre application est en cours d'exécution, et vous savez qu'elle va essentiellement consommer toutes vos ressources)

voici plus d'explication:

  • l'exécution de yield in loop garantira que cpu libérera l'exécution du thread, cependant, si le système essaie de revenir au thread, il répétera simplement l'opération yield. Cela peut permettre au thread d'utiliser 100% de la charge du noyau du processeur.
  • l'exécution de sleep() ou sleep_for() est également une erreur, cela bloquera l'exécution du thread mais vous aurez quelque chose comme le temps d'attente sur le processeur. Ne vous méprenez pas, ce IS cpu de travail mais sur la priorité la plus basse possible. Tout en travaillant en quelque sorte pour des exemples d'utilisation simples (cpu complètement chargé sur sleep () est moitié moins mauvais qu'un processeur de travail complètement chargé) , si vous voulez assurer la responsabilité de l'application, vous voudriez quelque chose comme le troisième exemple:
  • combiner! :

    std::chrono::milliseconds duration(1);
    while (true)
       {
          if(!mutex.try_lock())
          {
               std::this_thread::yield();
               std::this_thread::sleep+for(duration);
               continue;
          }
          return;
       }
    

quelque chose comme cela garantira, cpu produira aussi vite que cette opération sera exécutée, et sleep_for () s'assurera également que cpu attendra un certain temps avant même d'essayer d'exécuter la prochaine itération. Ce temps peut bien sûr être ajusté dynamiquement (ou statiquement) en fonction de vos besoins

à votre santé :)

4
Esavier

Ce que vous voulez, c'est probablement une variable de condition. Une variable de condition avec une fonction de réveil conditionnel est généralement implémentée comme ce que vous écrivez, avec le sommeil ou céder à l'intérieur de la boucle une attente sur la condition.

Votre code ressemblerait à:

std::unique_lock<std::mutex> lck(mtx)
while(!bSomeCondition) {
    cv.wait(lck);
}

Ou

std::unique_lock<std::mutex> lck(mtx)
cv.wait(lck, [bSomeCondition](){ return !bSomeCondition; })

Il vous suffit de notifier la variable de condition sur un autre thread lorsque les données sont prêtes. Cependant, vous ne pouvez pas y éviter un verrou si vous souhaitez utiliser la variable de condition.

1
shangjiaxuan