Native Threads und AppDomains



  • Ahoi zusammen.

    *seufz* Es passt mal wieder alles super zusammen:

    Wir haben hier bei uns im Team eine inhouse entwickelte Legacy-Library die einen Großteil unserer Grundimplementierungen enthält. Diese Library ist in nativem C++ entwickelt und verwendet verschiedene Komponenten des Qt Frameworks.

    Da wir einige Funktionalitäten aus dieser Library in unserem derzeitigen .NET Projekt verwenden sollen wurde ein bisschen Gluecode geschrieben, im Sinne eines C++/CLI Wrappers um die native Library herum.

    Jetzt ist das "Problem", dass unser Testteam NUnit für ihre Integrationstests verwendet und jeder Test sofort nach dem Start abschmiert. Wir haben jetzt also geguckt und geschaut woran das liegt, zumal der selbe Testcode ohne NUnit frei ausgeführt einwandfrei funktioniert.

    Das Ende vom Lied ist nun folgendes: Wenn der NUnit Prozess startet wird logischerweise eine AppDomain (ID 1) hochgezogen. Für jeden Test erstellt NUnit zur Kapselung eine weitere AppDomain (ID 2) in der der Test dann laufen wird. Sobald der Testcode im Laufe der ganzen Aufrufe irgendwann in unsere native Legacy-Library einsteigt wird eine Qt-Messagepunmp in einem separaten Thread erstellt. Dieser neue Thread wird allerdings in AppDomain 1 erstellt obwohl wir uns gerade in AppDomain 2 befinden. Der Rest ist dann einfach ein Faden der sich durchzieht und letztendlich zu dem Absturz führt.

    Mir ist bewusst, dass AppDomains ein Managed-Konzept ist, aber anscheinend hat sich da bei uns damals bis heute niemand Gedanken darüber gemacht, dass die Verwendung von Qt uns noch so in den Rücken fallen kann...

    Ich habe jetzt hier eben das Problem, dass ich eine native Lib habe die wir zwangsweise verwenden müssen, diese Qt verwendet was wir da nicht mehr so einfach rausnehmen können, und unser Testteam alles mit NUnit aufgebaut hat und da alles zusammenbricht.

    Kennt irgendjemand eine Möglichkeit, dass z.B. auch native Threads implizit in der aktuellen AppDomain erstellt werden? Ich meine, letztendlich wird auch die CLR vermutlich irgendwelche Kernelfunktionen oder derartiges aufrufen, vielleicht gibt's ja da irgendeine Brücke die man schlagen kann. Aber allgemein bin ich für jede gute Idee die mir helfen könnte sehr dankbar.

    Am Ende werden wir aber denke ich vermutlich doch Teile einfach reimplementieren müssen... 😕



  • Ich glaub nicht, dass das geht. Die Threads sind ja nicht mal in .NET an irgendwelche AppDomains gebunden.
    Du kannst aber unterbinden, dass NUnit weitere AppDomains erstellt.

    http://www.nunit.org/index.php?p=consoleCommandLine&r=2.5.5



  • Dein Probleme verstehe ich nicht ganz... warum sollte eine AppDomain auswirkungen auf Deinen nativen Thread haben?



  • Das Problem ist, dass der native Thread über Callbacks wieder in managed Code reinspringen kann, wo (managed) statische Variablen verwendet werden. Da der Thread (zumindest nach Ausgabe von AppDomain::CurrentDomain->Id ) immer in der primären AppDomain erstellt wird, und nicht in der AppDomain wo der Thread gespawned wurde, ergeben sich hier einige Probleme, da z.B. sämtliche .cctor von statischen Klassen erneut aufgerufen werden, was in meinem Fall die Fehler verursacht.

    Wenn ich über System::Threading::Thread einen neuen Thread erstelle, habe ich die Probleme nicht. Und auch wenn das MSDN sagt, dass Threads AppDomain-übergreifend sind, so wird der managed Thread beim Auswerten von AppDomain::CurrentDomain->Id zumindest theoretisch in der richtigen AppDomain erstellt.

    Es gibt also definitiv einen Unterschied ob ein Thread mit System::Threading::Thread oder einem QThread , bzw. einem CreateThread() erstellt wurde. Bei Letzterem werden zwangsläufig sämtliche statischen Variablen und Klassen für die primäre AppDomain neu initialisiert und vor allem auch verwendet, und das ist mein Problem.

    Ich habe bereits mit __declspec(process) und __declspec(appdomain) versucht ein bisschen was zu drehen, aber da war erwartungsgemäß nicht viel zu holen. Wenn ich versuche einen "echten" statischen gcroot für meinen Wrapper zu erzeugen, dann wird sofort beim Zugriff eine Exception geworfen, dass gcroot nicht AppDomain-übergreifend verwendet werden kann.

    Ich bin mir ziemlich sicher, dass sich das Problem sofort lösen liese, wenn ich den nativen Code anstatt über einen nativen Thread über einen managed Thread starte. Das Problem ist eben nur, dass ich derzeit keinen Zugriff auf die Sourcen habe und das ewig beim anderen Team einpflegen müsste - ob die dann einsehen was zu machen ist wieder eine andere Sache.

    Ich versuche vielleicht mal mein Szenario als knappes Beispiel zu posten.

    EDIT: @Mechanics: Stimmt, und das funktioniert auch wunderbar 🙂
    Jetzt besteht der Integrations-Test aber darauf mit der GUI arbeiten zu können, da sie hier am morgen gleich auf einen Schlag sehen auf welchen Systemen was schiefgelaufen ist und wo nicht. Kopf -> Wand. Aber ja, prinzipiell würde der Switch für die Konsole das Problem beheben.



  • Die Infos hier hast Du aber schon gelesen, oder?
    http://lambert.geek.nz/2007/05/unmanaged-appdomain-callback/
    http://lambert.geek.nz/2011/12/appdomains-and-unmanaged-callbacks-redux/

    Fazit: Wenn Du "GetFunctionPointerForDelegate" verwendest, sollte der Callback immer in der korrekten AppDomain erfolgen...



  • Die zwei Artikel sind mir bereits über den Weg gelaufen, hatte aber nach kurzem Überfliegen das Gefühl, dass sie zwar in etwa mit dem Thema zu tun haben aber nicht mein exaktes Problem lösen - Ich werd's mir aber sofort mal genauer durchlesen, vielleicht habe ich da etwas zu schnell geurteilt.

    In der Zwischenzeit hier mal ein simples und getrimmtes Example meines Problems. Sinn macht's nicht viel, aber es soll ja nur den Ablauf aufzeigen.

    Diese Dummy-App besteht aus drei Komponenten:

    1. Einem C# Test für NUnit,
    2. einer C++/CLI Wrapper-DLL um die native Libarary zu nutzen,
    3. und die native Lib an sich.

    Der Verlauf ist folgendermaßen: Der "Test" ruft eine statische Methode des Wrappers auf, der einen Thread erzeugt, der wiederum einen Handler als Callback erhält. In unserem Fall ist der Handler ein regulärer nativer Pointer der allerdings auf eine statische managed int Variable zugreift und diese ausgibt. Der Konstruktor setzt den Wert auf 'null', und er wird beim Erstellen des Threads auf 666 gesetzt.

    Im File 'Managed.cpp' kann jetzt durch auskommentieren der jeweiligen Zeilen zwischen dem Erstellen eines nativen oder eines managed Threads gewechselt werden. Beim managed Thread funktioniert der Test wie gedacht, beim nativen stürzt er aufgrund eines null-pointers ab (die statische Variable wird aus einer anderen AppDomain verwendet und ist dort noch 'nullptr').

    Verwende ich nur eine AppDomain funktionieren beide Varianten.

    Ich habe jetzt allerdings nicht den Luxus zu einem managed Thread zu wechseln da der ganze Code in dieser nativen Lib drin ist, und dort alles mit Qt verbandelt ist:

    Test.cs

    using System;
    using System.Threading;
    using NUnit.Framework;
    
    namespace AppDomainStatic {
    
    [TestFixture]
    public class Fixture {
        [SetUp]
        public void SetUp() {
            Console.WriteLine("====== SetUp() ======");
    
            // Call thread and print the static int value
            // (should be 666, but crashes if spawned
            // in another AppDomain due to nullptr).
            Managed.Funcs.StartThread();
    
            // Just wait some time to have the message print.
            Thread.Sleep(1000);
        }
    
        [Test]
        public void Test() {
            Console.WriteLine("====== Test() ======");
        }
    
        [TearDown]
        public void TearDown() {
            Console.WriteLine("====== TearDown() ======");
        }
    
    }
    
    }
    

    Managed.hpp

    #ifndef MANAGED_HPP
    #define MANAGED_HPP
    
    #include <vcclr.h>
    #include "../NativeProject/funcs.hpp"
    
    using namespace System;
    
    namespace Managed {
    
    struct Handler : IHandler {
        void handle(void);
    };
    
    public ref class Funcs abstract sealed {
        private:
            static Handler
                *_handler;
    
            static int
                *_someValue;
    
            static void ThreadProc(void);
    
        public:
            static property int SomeValue {
                int get(void);
            }
    
            static void StartThread(void);
            static Funcs(void);
    };
    
    }
    
    #endif
    

    Managed.cpp

    #include <iostream>
    #include <Windows.h>
    #include "managed.hpp"
    
    using namespace System::Threading;
    
    namespace Managed {
    
    void Handler::handle(void) {
        Console::WriteLine("Value {0}", Funcs::SomeValue);
    }
    
    static Funcs::Funcs(void) {
        _handler = new Handler;
        _someValue = nullptr;
    }
    
    int Funcs::SomeValue::get(void) {
        return *_someValue;
    }
    
    void Funcs::ThreadProc(void) {
        ::threadProc(_handler);
    }
    
    void Funcs::StartThread(void) {
        _someValue = new int(666);
    
        // Uncomment for NATIVE thread
        //::createThread(_handler);
    
        // Uncomment for MANAGED thread
        //Thread^ t = gcnew Thread(gcnew ThreadStart(Funcs::ThreadProc));
        //t->Start();
    }
    
    }
    

    Native.hpp

    #ifndef FUNCS_HPP
    #define FUNCS_HPP
    
    #ifdef NATIVEPROJECT_SYMEXPORT
    #   define NATIVEPROJECT_SYMSPEC __declspec(dllexport)
    #else
    #   define NATIVEPROJECT_SYMSPEC __declspec(dllimport)
    #endif
    
    struct IHandler {
        virtual void handle(void) = 0;
    };
    
    extern DWORD NATIVEPROJECT_SYMSPEC WINAPI threadProc(void* data);
    extern void NATIVEPROJECT_SYMSPEC createThread(IHandler* handler);
    
    #endif
    

    Native.cpp

    #include <iostream>
    #include <Windows.h>
    #include "funcs.hpp"
    
    DWORD WINAPI threadProc(void* data) {
        if(nullptr != data) {
            (reinterpret_cast<IHandler*>(data))->handle();
        }
    
        return 0;
    }
    
    void createThread(IHandler* handler) {
        DWORD threadId = 0;
        HANDLE h = CreateThread(nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(&threadProc), handler, 0, &threadId);
    }
    


  • Wie gesagt: Durch verwendung von "GetFunctionPointerForDelegate" löst sich das Problem ganz einfach...
    Du musst also nur Deinen C++/CLI-Wrapper anpassen...

    Hier mal ein vollständiges Beispiel (einfach in ein C++/CLI-Projekt kopieren und ausführen):

    #include <Windows.h>
    
    using namespace System;
    
    delegate void MyDelegate();
    typedef void *__stdcall pMyDelegate();
    IntPtr CallbackPointer;
    
    void UnmanagedCallback()
    {
      pMyDelegate *callback = (pMyDelegate*)CallbackPointer.ToPointer();
      callback();
    }
    
    #pragma unmanaged
    DWORD __stdcall MyNativeThread(LPVOID)
    {
      UnmanagedCallback();
      return 0;
    }
    void StartNativeThread()
    {
      CloseHandle(CreateThread(NULL, 0, &MyNativeThread, NULL, 0, NULL));
    }
    #pragma managed
    
    namespace Foo
    {
      public ref class OwnAppDomainClass : MarshalByRefObject
      {
      public:
        void Test()
        {
          System::Console::WriteLine("OwnAppDomainClass::Test: Domain: {0}", System::AppDomain::CurrentDomain->FriendlyName);
          auto del = gcnew MyDelegate(Callback);
          CallbackPointer = System::Runtime::InteropServices::Marshal::GetFunctionPointerForDelegate(del);
          StartNativeThread();
    
          System::Threading::Thread::Sleep(1000);
          GC::KeepAlive(del);
        }
        static void Callback()
        {
          System::Console::WriteLine("OwnAppDomainClass::Callback: Domain: {0}", System::AppDomain::CurrentDomain->FriendlyName);
        }
      };
    }
    
    int main()
    {
      Console::WriteLine(L"Main: Domain: {0}", System::AppDomain::CurrentDomain->FriendlyName);
    
      System::AppDomain^ newAppDomain = System::AppDomain::CreateDomain("NewApplicationDomain");
    
      System::String^ str = (gcnew Uri(System::Reflection::Assembly::GetEntryAssembly()->CodeBase))->AbsolutePath;
      // Create an instance of RemoteObject:
      auto a = newAppDomain->CreateInstanceFrom(str, "Foo.OwnAppDomainClass");
      auto b = a->Unwrap();
    
      Foo::OwnAppDomainClass^ crossAppInstance = static_cast<Foo::OwnAppDomainClass^>(b);
    
      crossAppInstance->Test();
    
      return 0;
    }
    

    Der "Trick" befindet sich in "UnmanagedCallback". Dort wird einfach der Delegate-Pointer *direkt* aufgerufen (als normale C-Funktion). Dadurch weißt das .NET diesem Pointer für die Laufzeit des Aufrufes die "richtige" Domain zu (also diese, wo das Delegate erzeugt wurde!)
    Mit diesem Delegate-Pointer wird sozusagen eine Wrapper-Methode erzeugt, welche intern zuerst die AppDomain zuweist und erst dann die eigentliche managed Methode aufruft!

    PS: Du musst nur noch Sicherstellen, dass das Delegate in der zwischenzeit nicht vom GC aufgeräumt wird. Entweder durch pinnen (nicht empfohlen), oder durch passendes einfügen von "GC::KeepAlive(delegate)" z.B. im Destruktor der Klasse, wo der Delegate verwendet wird.

    PPS: Hab das Beispiel nochmals angepasst und KeepAlive eingebaut



  • Sir, you are my hero! 🙂

    Funktioniert einwandfrei! Da die Handler allerdings in Wirklichkeit in dem signal/slot Konstrukt von Qt versteckt sind und dort aufgerufen wurden, und mein Thread eigentlich ein QThread war, musste ich das auf einer etwas höheren Ebene verbandeln, aber es läuft jetzt wie geschmiert.

    Konnte sogar ein bisschen Codebloating reduzieren und das ganze etwas wartbarer machen, schöner Nebeneffekt.

    Tausend Dank für den Hinweis auf die Delegates!



  • InterOp ist gar nicht so schwer, wie man immer sagt 😉


Anmelden zum Antworten