bug avec les sockets et les timers sous Windows

Le timer déclenche la méthode avant la fin l'appel précédent.

a marqué ce sujet comme résolu.

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.

Peut-être à cause de la période ? (le paramètre que tu as mis à 10)

Le timer semble créer un nouveau processus (léger).. ..donc ils peuvent s’exécuter en même temps, qu'importe si un précédent processus a été lancé je suppose ?

Je ne connais pas suffisament l'API Windows, mais la solution doit se trouver dans cette page : MSDN : SetWaitableTimer

Hmm.. sinon, penses bien à gérer les erreurs, comme sur cet exemple : MSDN : Using Waitable Timer Objects


Pourquoi ne pas lancer un timer à partir de ta routine, ou après le wait et utiliser liDueTime ? :D

+0 -0

Peut-être à cause de la période ? (le paramètre que tu as mis à 10)

Le timer est censé attendre la fin de l'exécution précédente avant de lancer la suivante (et c'est ce qui se passe si je remplace le sendto par une boucle longue ou un appel à sleep). Donc, ça n'explique pas le problème. J'ai d'ailleurs tenté avec une période bien plus haute et le problème survient tout de même.

Le timer semble créer un nouveau processus (léger).. ..donc ils peuvent s’exécuter en même temps, qu'importe si un précédent processus a été lancé je suppose ?

Je n'ai pas l'impression que le timer crée un nouveau thread. Qui plus est, si c'était le cas, on verrait au moins la sortie de timerProcess apparaître, au bout d'un moment. Pourtant, il manque bien la ligne << End TimerProcess correspondant.

Je ne connais pas suffisament l'API Windows, mais la solution doit se trouver dans cette page : MSDN : SetWaitableTimer

Les infos du MSDN confirme bien que le timer appelle la routine si le thread est en attente passive (j'étais passé à côté, merci).
Mais ce que je ne comprends pas c'est pourquoi sendto prend autant de temps (le problème survient même si je mets 320ms comme période) et pourquoi ça n'arrive pas à chaque fois et toujours au second passage, dans ce cas. Je ne comprends pas non plus pourquoi sendto passerait le thread en attente passive alors qu'il est censé être synchrone et que le timer lance quand même l'appel à la routine…

Hmm.. sinon, penses bien à gérer les erreurs, comme sur cet exemple : MSDN : Using Waitable Timer Objects

Les erreurs sont bien gérées, ne t'en fais pas. J'ai juste réduit au maximum le code pour ne pas trop compliqué le tout.

Pourquoi ne pas lancer un timer à partir de ta routine, ou après le wait et utiliser liDueTime ?

Parce que c'est beaucoup plus compliqué que ça, en réalité. La routine n'est pas appelée directement par le timer qui gère beaucoup d'autres éléments.

En tout cas, merci de ton aide.

Salut!

Essaie d'attendre que la totalité des données soient envoyées par sendto (qui peut renvoyer une valeur inférieure à bufLen)

1
2
3
4
5
6
7
int offset = 0;
while(bufLen > 0)
{
    int nbBytes = sendto(socketId, buffer+offset, bufLen, 0, (SOCKADDR *) & destAddr, sizeof (destAddr));
    bufLen -= nbBytes;
    offset += nbBytes;
}
+0 -0

La seule chose qui a pu aider ici est le lien ^^'

SylafrsOne

Tu m'as surtout permis de voir un détail de la doc qui m'a confirmé mes intuitions (que les timers profitent de la mise en pause induite par sendto).

Essaie d'attendre que la totalité des données soient envoyées par sendto (qui peut renvoyer une valeur inférieure à bufLen)

Fraggy

Mes datagrammes ne font que 12 octets (ce sont des marqueurs de temps pour la synchronisation des différents acteurs). Donc, je ne pense pas que ça pose problème.

Par contre, il semblerait que sendto mette énormément de temps à la première exécution car il attend du noyau la création et l'enregistrement d'une socket d'émission. Cette socket semble être sauvegardée pour être utilisée pour les prochains envois, histoire de gagner du temps. Ce doit être pendant le bind de cette socket que le thread se met en pause permettant au timer d'exécuter sa routine.

Bref, je pense que si je veux une réponse à ma question, il faudrait que je me plonge dans les sources de l'API Windows…

Merci pour votre aide.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte