Salut les zesteurs,
Je travaille actuellement sur un projet C++ sous Windows qui consiste en un logiciel chargé de recevoir des données en UDP d'un acteur et de les transmettre aux autres acteurs (un peu comme un switch réseau, grosso modo). Mais je suis confronté à un bug non reproductible que je n'arrive pas à expliquer.
Voici le code minimal qui reproduit le bug avec les mêmes conditions que le projet :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | int socketId = 0; VOID CALLBACK timerProcess( LPVOID lpArg, // Data value DWORD dwTimerLowValue, // Timer low value DWORD dwTimerHighValue ) // Timer high value { std::cout << ">> End TimerProcess" << std::endl; sockaddr_in destAddr; destAddr.sin_family = AF_INET; destAddr.sin_port = htons(4440); destAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //--------------------------------------------- // Send a datagram to the receiver int bufLen = 10; char buffer[10]; int nbBytes = sendto(socketId, buffer, bufLen, 0, (SOCKADDR *) & destAddr, sizeof (destAddr)); if (nbBytes == SOCKET_ERROR) { std::cout << "sendto failed with error: " << WSAGetLastError() << std::endl; } std::cout << "<< Start TimerProcess" << std::endl; } int main( int argc, char** argv ) { // Initialize Winsock WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); socketId = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); HANDLE hTimer; LARGE_INTEGER liDueTime; liDueTime.LowPart = 0; liDueTime.HighPart = 0; hTimer = CreateWaitableTimer( NULL, // Default security attributes FALSE, // Don't Create auto-reset timer NULL); // No name SetWaitableTimer( hTimer, // Handle to the timer object &liDueTime, // Timer will become signaled immediatly 10, // Periodic timer interval of 60 ms timerProcess, // Completion routine NULL, // Argument to the completion routine FALSE ); // Do not restore a suspended system while ( 1 ) { WaitForSingleObjectEx( hTimer, INFINITE, TRUE ); } CloseHandle( hTimer ); } |
Le principe est le suivant : je crée un timer qui exécutera toutes les 60 ms une routine timerProcess
chargée d'envoyer via une socket UDP un datagramme à un acteur spécifique. Une fois le timer lancé, je me mets en attente passive jusqu'à la fin du programme.
Tout fonctionne comme je le souhaite. Cependant, j'ai un bug qui arrive régulièrement mais pas à chaque fois (le code peut fonctionner 100 fois de suite comme planter 9 fois sur 10…).
En fait, il arrive que le deuxième appel à la méthode timerProcess()
se fasse avant la fin de l'exécution du premier. On peut le voir sur l'extrait de la sortie suivant :
1 2 3 4 5 6 7 | >> Start TimerProcess >> Start TimerProcess sendto failed with error: 10022 << End TimerProcess >> Start TimerProcess sendto failed with error: 10022 << End TimerProcess |
L'erreur 10022 (argument invalide) retournée par la socket correspond typiquement à un accès à une socket en cours d'utilisation.
Grâce au debugger, je me rends compte que lorsqu'on arrive à l'instruction sendto
de timerProcess
, on est re-routé magiquement vers un nouvel appel à timerProcess
. Et étant donné que sendto
protège la socket avec un mutex durant l'envoi, au deuxième appel, on se retrouve à vouloir utiliser une socket invalide.
Ce que je n'arrive pas à comprendre, c'est comment se fait-il que le timer puisse appeler une deuxième fois la méthode timerProcess
sans attendre la fin de la première exécution ?
J'ai essayé en remplaçant le code de timerProcess
par une boucle très longue voire un sleep()
, mais à chaque fois le timer n'exécutait le second appel qu'à la sortie de la méthode même si elle durait une minute…
Je pense que c'est dû à sendto
qui doit mettre en pause le thread le temps que le noyau initialise une socket d'envoi. Le timer arrive à terme, voit que le thread est en attente et le réveille pour appeler timerProcess
.
Mais je ne suis pas vraiment convaincu de ma théorie et n'étant pas vraiment habitué à la programmation sous Windows, j'ai l'impression de louper quelque chose…
Corriger le code est assez simple : il suffit d'ajouter un flag dans timerProcess
pour empêcher d'exécuter le code tant que l'appel précédent n'est pas terminé. C'est surtout le pourquoi qui m'intéresse.
Je suis ouvert à toute suggestion.
Merci.