Warten auf Callback



  • folgendes Problem unter Windows (MFC):
    - ich benutze eine drittanbieter-dll die (asynchron) mit einer hardware komuniziert und das ergebnis ihrer kommunikation per callback an mein programm zurückliefert (oder aucht nicht => timeout)

    frage dazu: wie bekomme ich das ganze am besten synchroniesiert? (mein eigenes programm ist single-threaded, jeder aufruf an die dritt-anbieter-dll soll synchron ein ergebnis an meinen client liefern)

    im ersten wurf kam mir folgender gedanke:

    ULONG ulDLLResult = 0; 
    
    //Callback-Funktion, die mit Ergebnis aufgerufen wird
    void CALLBACK EXPORT CallbackResult(ULONG ulRes){
      ulDLLResult = ulRes;
    }
    
    //soll auf Aufruf des Callbacks warten 
    //(wie stellt man sowas am schlauesten an?)
    ULONG WaitForCallBack(){
      ULONG ulRetVal = 0;
      int iTimeout = 1000;
    
      for (int iWait=0; ulDLLResult == 0 && iWait < iTimeOut; iWait++){
        if (ulRetVal != 0){
          //Callback wurde aufgerufen
          ulRetVal = ulDLLResult;
        }
        else{
          //Callback wurde (noch) nicht aufgerufen
          ::Sleep(1);
        }
      }
    
      return ulRetVal;
    }
    
    //Vom Client aufzurufen, erwartet synchron einen Wert
    ULONG ClientFunction(){
      //Initialisierung Merker "Callback nicht aufgerufen"
      ulDLLResult = 0;
    
      //Drittanbieter-Funktion aufrufen, die (irgendwann) meinen Callback
      //aufruft oder auch nicht
      CallDLLFunctionThatCallsCallbackAsynchronous();
    
      ulDLLResult = WaitForCallBack();
      return ulDllResult;
    }
    

    was hält die fachwelt davon? ist das mit dem sleep ein gangbarer, sinnvoller weg?
    wenn ja, ist es besser, 1000x sleep(1) oder 100x sleep(10) zu benutzen?
    wenn nein, was ist eine besser (bzw. die beste) möglichkeit, soetwas zu implementieren?

    gruß und dank

    micha



  • Keine Ahnung was die Fachwelt davon hält... 🙂 Wenn es funktioniert ist es doch in Ordnung. 10xSleep(100) würde auch reichen, denke ich jetzt mal.



  • RR schrieb:

    Wenn es funktioniert ist es doch in Ordnung.

    klar, nur müsste man wissen, was genau "funktioniert" bedeutet 😉
    wenn mein system länger als nötig wartet, wenn irgendwelche anderen anwendungen blockieren, wenn das gui nicht mehr reagiert, wenn mehr speicher oder prozessorzeit als nötig verbraucht wird, ... (liste beliebig fortsetzbar), dann führt das in den status "funktioniert nicht"

    RR schrieb:

    10xSleep(100) würde auch reichen, denke ich jetzt mal.

    was meinst du denn mit "reicht"? reicht wofür?
    was hat 10x100 gegenüber 1000x1 für vorteile?
    nachteil ist ganz klar, ich warte 100ms, auch wenn mein callback bereits nach 10ms aufgerufen wurde



  • sowas löst man mit einem EVENT

    CreateEvent: http://msdn.microsoft.com/en-us/library/aa915030.aspx
    Sowie SetEvent, ResetEvent und WaitForSingleObject



  • Ja, da gehört ein Event hin. Dein Callback wird wahrscheinlich sowieso in einem anderen Thread aufgerufen: der Compiler dürfte Dir die Tests auf uDllResult wegoptimieren, die CPU muß Dir die Veränderung gar nicht zeigen.

    HANDLE event = CreateEvent(NULL, false, false, NULL);  // automatic reset, initially reset
    
    volatile ULONG ulDLLResult = 0; 
    
    //Callback-Funktion, die mit Ergebnis aufgerufen wird
    void CALLBACK EXPORT CallbackResult(ULONG ulRes){
      ulDLLResult = ulRes;
      SetEvent(event);
    }
    
    ULONG WaitForCallBack()
    {
       WaitForSingleObject(event);
       return ulDLLResult;
    }
    

    ---------

    Alternativ kannst Du Dir in der CallBack_Funktion mittels PostMessage eine Windows-Message an ein fenster des Main Thread schicken (z.B. >= WM_APP). Das macht es einfacher, das Nutzerinterface während der Verarbeitung "aktiv" zu halten, verwurstelt aber UI und Abarbeitung.



  • peterchen schrieb:

    der Compiler dürfte Dir die Tests auf uDllResult wegoptimieren, die CPU muß Dir die Veränderung gar nicht zeigen.

    Dürfte vielleicht, aber aktuelle Windows-Compiler können das nicht, da sie nicht wissen was Sleep() macht. Was die CPU angeht: da Sleep() garantiert eine Read-Write-Barrier enthält, kann auch die CPU da nicht dreinfunken. Mal ganz davon abgesehen dass x86 CPUs das sowieso nicht machen.

    Theoretisch wären aber Compiler denkbar die wissen was Sleep() macht, wissen dass es nix im Speicher des Programms ändert, und daher wirklich das Lesen des Wertes vor die Schleife rausziehen. Wäre cool, dann würden nämlich viele Programme von Leuten mit l33t c0d1ng sk1llz nimmer funktionieren 🙂



  • peterchen schrieb:

    Ja, da gehört ein Event hin. Dein Callback wird wahrscheinlich sowieso in einem anderen Thread aufgerufen:

    vermutlich, ja

    der Compiler dürfte Dir die Tests auf uDllResult wegoptimieren, die CPU muß Dir die Veränderung gar nicht zeigen.

    hm, das versteh ich jetzt nicht ganz genau. wie schauts damit aus:
    was ich gepostet habe, war natürlich eine vereinfachter schnipsel aus meinem code.
    wie siehts aus, wenn ich keinen globalen ulong nehme, sondern statische klassenmember? die reale welt sieht bei mir eher so aus:

    class CMyDriver{ 
    	static void CALLBACK EXPORT CallbackResult(ULONG ulRes);
    
    	struct CallbackResult{             
    		int iCaller;
    		ULONG ulResult;
    	};
    
    	static CallbackResult CALLBACK_RESULT;
    }
    

    muß dann auch der static-member (CALLBACK_RESULT) als volatile gekennzeichnet werden? doch eher nicht, oder? zumindest, solange es eine instanz von CMyDriver gibt, meine ich

    > (dein cpp-code)
    ja, für WaitForSingleObject hab ich mich dann auch entscheiden.
    vor dem Aufruf des Callback-Initiators noch ein beherztes

    ResetEvent
    

    und dann ist man im grunde auch durch bzw. es gibt nur noch eine "theoretische chance", daß da was schiefgeht, die muß ich nicht weiter betrachten

    micha



  • Wenn du den EVENT korrekt einsetzt, dann gibt es gar keine Chance mehr dass was schief geht, auch keine theoretische.

    Und wenn du es ohne EVENT und volatile machst, dann gibt es - je nachdem was dein Code sonst noch macht - auch eine ganz praktische Chance dass was schief geht.

    Versuch z.B. mal folgendes:

    int my_signal = 0;
    
    void thread_fn()
    {
        Sleep(100);
        my_signal = 1;
    }
    
    void foo()
    {
        assert(my_signal == 0);
        starte_thread(thread_fn);
        while (my_signal == 0)
        {
            // gar kein code hier, oder zumindest keiner der irgendwelche funktionen aufruft
        }
        assert(my_signal == 1);
    }
    

    Der Compiler wird den Test "my_signal == 0" vor die Schleife rausziehen, da er glaubt sicher sein zu können, dass "my_signal" während die Schleife läuft nicht verändert wird.
    -> das Programm wird nie terminieren

    Fügst du ein Sleep() in der Schleife ein, oder auch nur einen InterlockedXxx() Befehl, geht es auf einmal.



  • hustbaer schrieb:

    Wenn du den EVENT korrekt einsetzt, dann gibt es gar keine Chance mehr dass was schief geht, auch keine theoretische.

    dann mach ich wohl etwas nicht korrekt...
    (pseudcode):

    //.h                                  
    class MyDriver{    
    	static void CALLBACK mycallback(int callbackdata);
    	static bool WaitForCallBack();  
    	bool ClientFunction(int& iResult);
    
    	HANDLE event;
    
    	struct CallBackData{
    		int iMarker;
    	}	
    	static CallBackData MYDATA;
    }
    
    //.cpp
    HANDLE MyDriver::event = CreateEvent(NULL, false, false, NULL);  // automatic reset, initially reset     
    MyDriver::CallBackData MyDriver::MYDATA;
    
    void CALLBACK MyDriver::mycallback(int callbackdata){     
    	//hier irgendetwas befüllen, das als static MyStruct deklariert ist   
    	MYDATA.iMarker=callbackdata;
    	SetEvent(event);
    }    
    
    bool MyDriver::WaitForCallBack() 
    {   
    	bool bRetVal = false;
     	DWORD dwWaitResult=WaitForSingleObject(event, 1000);            
     	if (dwWaitResult == WAIT_OBJECT_0){
    		//Callback wurde aufgerufen => Ergebnis verarbeiten     
    		bRetVal = true;
    	}
    	else{
    		//Timeout
    	}
    	return bRetVal; 
    }                    
    
    //Vom Client aufzurufen, erwartet synchron einen Wert 
    bool MyDriver::ClientFunction(int& iResult){ 
      //Initialisierung Merker "Callback nicht aufgerufen" 
      ResetEvent(event); 
    
      //Drittanbieter-Funktion aufrufen, die (irgendwann) meinen Callback 
      //aufruft oder auch nicht 
      CallDLLFunctionThatCallsCallbackAsynchronous(); 
    
      bool bCalled = WaitForCallBack();  
      if (bCalled){
      	iResult = MYDATA.iMarker;
      }
    
      return bCalled; 
    }
    

    so weit "korrekt eingesetzt"?

    und nun stell dir folgendes szenario vor:
    - ClientFunction wird aufgerufen
    - Callback wird NICHT innerhalb der 1000 ms aufgerufen
    - => WaitForCallBack returniert false, ClientFunction returniert false => client weiß, daß timeout auftrat und reagiert entsprechend
    - ClientFunction wird nochmal aufgerufen
    - direkt NACH ResetEvent und VOR WaitForCallBack (bspw. zeile 43) wird nun (doch noch) der Callback des ersten aufrufs von ClientFunction aufgerufen (Callback wird asynchron aufgerufen, daß er innerhalb von 1000 ms aufgerufen wird, ist nur wahrscheinlich, nicht sicher, insofern weiß ich nie genau, ob er noch kommt oder nicht)
    - => WaitForCallBack returniert sofort (ohne zu warten), jedoch mit dem ergebnis des callbacks des ersten aufrufs, nicht des zweiten

    das ist das, was ich mit "kann theoretisch noch immer schiefgehen" meinte - praktisch wird der fall wohl nie eintreten, zumal ich die antwort im gut-fall innerhalb weniger ms und im schlecht-fall nie erwarte.
    klar soweit oder was mach ich dann noch verkehrt?

    hustbaer schrieb:

    Und wenn du es ohne EVENT und volatile machst, dann gibt es - je nachdem was dein Code sonst noch macht - auch eine ganz praktische Chance dass was schief geht.

    vom sleep bin ich inzwischen weg (obwohl es noch immer keinen konkreten grund in meinen augen gibt ("das macht man so" ist nichts konkretes)), dennoch bleibt für mich die Frage, wenn ich WaitForSingleObject benutze, muß dann der static-klassenmember (im beispiel MYDATA), oder irgendetwas anderes, auch volatile sein? eher nicht, oder? (weiß btw. auch garnicht, ob das so einfach ginge)

    micha



  • hustbaer schrieb:

    Und wenn du es ohne EVENT und volatile machst, dann gibt es - je nachdem was dein Code sonst noch macht - auch eine ganz praktische Chance dass was schief geht.

    Darauf wollte ich hinaus 🙂
    Garantiert Sleep() tatsächlich eine Memory Barrier? (x64 ist ja nicht so weit weg...) In der MSDN finde ich nur
    "Functions that enter or leave critical sections, Functions that signal synchronization objects, Wait functions, Interlocked functions". (hier)

    [quote=suriel]vom sleep bin ich inzwischen weg (obwohl es noch immer keinen konkreten grund in meinen augen gibt ("das macht man so" ist nichts konkretes)), [/quote]

    Man sollte zumindest erstmal drauf hören, aber nachzufragen warum eigentlich so und nicht anders ist ein guter nächster Schritt 🙂

    Polling ist jedenfalls erstmal unhöflich anderen Programmen gegenüber: aller 1..20 ms erzwingst du einen Context switch (~10k cycles IIRC), außerdem schmeißt dein Code und deine Daten die gerade laufenden aus dem Cache. Im Ernstfall wird die CPU nicht runtergetaktet, also auch nix mit Stromsparen und Lüfter aus. Drittens... naja, sieht's halt amateurhaft aus 😉



  • suriel schrieb:

    dennoch bleibt für mich die Frage, wenn ich WaitForSingleObject benutze, muß dann der static-klassenmember (im beispiel MYDATA), oder irgendetwas anderes, auch volatile sein? eher nicht, oder?

    wenn du WaitForSingleObject/EnterCriticalSection/InterlockedXxx verwendest, dann brauchst du kein volatile mehr.
    sowohl der compiler als auch die CPU "respektieren" diese funktionen als barrier.

    Garantiert Sleep() tatsächlich eine Memory Barrier? (x64 ist ja nicht so weit weg...)

    ar, sorry. ich meine mit "garantiert" dass es im moment sicher so ist. zumindest wenn der compiler nicht dazwischenpfuscht. genaugenommen lehne ich mich auch dabei noch etwas weit aus dem fenster, aber ich kann mir grad keine implementierung von Sleep() vorstellen, die ganz ohne interlocked befehle auskommt. und auf x86/x64 ist jeder interlocked befehl eine volle read-write-barrier. um 100% sicher zu sein müsste man aber natürlich den code gesehen haben, und das habe ich nicht 🙂

    garantiert in dem sinn wird es aber vermutlich nirgends.


Anmelden zum Antworten