2016-09-03 4 views
6

J'ai rencontré une situation difficile à déboguer dans un de mes vrais projets où j'accédais accidentellement à une référence à une variable locale dans un lambda qui avait été déplacé. L'accès se faisait à partir d'un autre thread, mais le lambda déplacé était maintenu en vie jusqu'à ce que le deuxième thread soit terminé.Attraper et déboguer une utilisation invalide de la référence à une variable locale à l'intérieur de lambda

Le bug ne s'est produit qu'avec des optimisations désactivées et a été causé par un refactoring négligent.

J'ai créé un exemple minimal (available here on wandbox) qui reproduit la question:

struct state 
{ 
    int x = 100; 
}; 

template <typename TF> 
void eat1(TF&& f) 
{ 
    // Call the lambda. 
    f(); 

    // Simulate waiting for the second thread 
    // to finish. 
    std::this_thread::sleep_for(1000ms); 
} 

template <typename TF> 
void eat0(TF&& f) 
{ 
    // Move the lambda to some other handler. 
    eat1(std::forward<TF>(f)); 
} 

void use_state(state& s) 
{ 
    // Will print `100`. 
    std::cout << s.x << "\n"; 

    // Separate thread. Note that `s` is captured by 
    // reference. 
    std::thread t{[&s] 
     { 
      // Simulate computation delay. 
      std::this_thread::sleep_for(500ms); 

      // Will print garbage. 
      std::cout << s.x << "\n"; 
     }}; 

    t.detach(); 
} 

int main() 
{ 
    eat0([] 
     { 
      // Local lambda variable that will be accessed 
      // after the lambda is moved. 
      state s; 

      // Function that takes `s` by reference and 
      // accesses it in a separate thread after the 
      // lambda is moved. 
      use_state(s); 
     }); 
} 

Étonnamment, aucun des assainissants et drapeaux d'avertissement a réussi à aider.

J'ai essayé les combinaisons suivantes de compilateurs et assainissants, avec

-Wall -Wextra -Wpedantic -g -O0 

drapeaux toujours activés:

  • Compilateurs: g ++ 6.1.1 sur Arch Linux x64 ; clang ++ 3.8.0 sur Arch Linux x64; g ++ 5.3.1 sur Fedora x64; clang ++ 3.7.0 sur Fedora x64.

  • Désinfectants: -fsanitize=address; -fsanitize=undefined, -fsanitize=thread.

Aucune des combinaisons n'a produit de diagnostic utile. Je me attendais soit AddressSanitizer pour me dire que j'accédait à une référence ballants ou UndefinedSanitizer pour attraper UB lors de l'accès, ou ThreadSanitizer pour me dire un thread séparé accédait à un emplacement de mémoire non valide.

Existe-t-il un moyen fiable de diagnostiquer ce problème? Est-ce que je devrais publier cet exemple sur l'un des suiveurs de bogues de désinfectants en tant que demande/défaut de fonctionnalité?

+0

C++ est compliqué. Il n'y a rien de mal à capturer par référence. Cela fait partie de la langue. Si le timing des threads est différent, ceci est bien défini.Je ne vois pas comment le compilateur peut déduire le timing des threads d'exécution. Vous ne pouvez pas compter sur le compilateur tout attraper. –

+0

Eh bien, j'ai essayé l'analyse statique de MSVS qui utilise les lignes directrices de base de CPP et il ne l'a pas non plus détecté. Soit cette vérification n'a pas encore été implémentée, soit il n'existe pas encore de règle couvrant ce cas. Je ne sais pas si vous voulez soulever un problème pour voir si cela pourrait être diagnostiqué. [link] (https://github.com/isocpp/CppCoreGuidelines) – NathanOliver

Répondre

4

L'outil de contrôle de débit de valgrind a détecté ce problème avec les paramètres par défaut. Cependant, ce genre de bugs méchants ont des chances d'échapper à memcheck. Je ne suis pas sûr que le problème serait pris sur le vrai programme.

Le fait que le premier lambda ait été déplacé n'est pas pertinent pour le problème (bien que cela puisse compliquer le processus de débogage). Le problème est dû à l'accès à une variable locale dans une fonction qui a fini son exécution (encore une fois, le fait que l'accès ait eu lieu à partir d'un thread différent a compliqué l'investigation mais n'a pas contribué au bug). Le fait que le premier lambda ait été maintenu en vie ne devrait en aucun cas vous protéger - les variables locales appartiennent à l'invocation lambda et non le lambda lui-même.

+0

J'ai couru cela à travers valgrind aussi. valgrind ne s'est pas plaint, pas un coup d'oeil. –