c # - Er der nogen måde at arbejde rundt OS Loader Lock Deadlocks forårsaget af tredjeparts biblioteker?

Indlæg af Hanne Mølgaard Plasc

Problem



Jeg har et interessant problem, som jeg ikke har set dokumenteret andetsteds (i det mindste ikke dette specifikke problem).


Dette problem er en kombination af COM, VB6 og .NET og får dem til at spille godt.


Her er hvad jeg har:



  • En tidligere VB6 ActiveX DLL (skrevet af os)

  • En multi-threaded Windows-tjeneste skrevet i C #, der behandler anmodninger fra klienter over netværket og sender resultater tilbage. Det gør det ved at oprette en ny STA-tråd til at håndtere hver anmodning. Hver request-handler-tråd instanserer et COM-objekt (defineret i ActiveX DLL) for at behandle anmodningen og få resultatet (en streng af XML er sendt ind, og den returnerer en streng af XML tilbage), frigiver eksplicit COM-objektet og udgange. Tjenesten sender derefter resultatet tilbage til klienten.

  • Alt netværkskoden håndteres ved brug af asynkron netværk (dvs. trådtrådets tråde).



Og ja, jeg ved, at det allerede er en risikabel ting at gøre i første omgang, da VB6 ikke er meget venlig med multi-threaded applikationer til at begynde med, men det er desværre det, jeg sidder fast med i øjeblikket.


Jeg har allerede rettet en række ting, der forårsagede deadlocks i koden (for eksempel at sikre, at COM-objekterne faktisk oprettes og kaldes fra en separat STA-tråd, og sørg for at eksplicit frigive COM-objekter, før tråden går ud for at forhindre deadlocks, der opstod mellem affaldssamleren og COM Interop-koden osv.), men der er et deadlock-scenario, som jeg bare ikke kan synes at løse.


Med hjælp fra WinDbg kunne jeg finde ud af hvad der sker, men jeg er ikke sikker på, hvordan (eller hvis) der er en vej rundt om denne særlige dødgang.


Hvad sker der


Hvis en forespørgselshåndterings tråd forlader, og en anden forespørgselshandler tråd starter samtidig, kan der opstå en deadlock på grund af den måde, hvorpå VB6-runtime-initialiserings- og opsigelsesrutinerne ser ud til at fungere.


Døden opstår i følgende scenario:



  • Den nye tråd, der starter op, er midt i oprettelsen af ​​en ny forekomst af (VB6) COM-objektet til behandling af en indgående forespørgsel. På dette tidspunkt er COM-runtime midt i et opkald for at hente objektets klassefabrik. Implementeringen af ​​klassefabrikken er i selve VB6-spilletiden ( MSVBVM60.dll ). Det er dens kalder VB6 runtime s DllGetClassObject funktion. Dette kalder på sin side en intern runtime funktion (MSVBVM60!CThreadPool::InitRuntime), som erhverver en mutex og går ind i en kritisk sektion for at udføre en del af sit arbejde. På dette tidspunkt skal den ringe LoadLibrary for at indlæse oleaut32.dll i processen, mens du holder denne mutex. Så nu holder den denne interne VB6 runtime mutex og venter på OS Loader Lock.

  • Den tråd, der springer ud, kører allerede inde i loader-låsen, fordi den er færdig med at håndtere administreret kode og udføres inden for funktionen KERNEL32! ExitThread . Det er specifikt det midt i håndtering af DLL\_THREAD\_DETECH beskeden til MSVBVM60.dll på den tråd, der igen kalder en metode til at afslutte VB6 runtime på tråden (MSVBVM60!CThreadPool::TerminateRuntime). Nu forsøger denne tråd at erhverve den samme mutex, at den anden tråd, der initialiseres, allerede har.



En klassisk dødlås. Tråd A har L1 og ønsker L2, men tråd B har L2 og har brug for L1.


Problemet (hvis du har fulgt mig så langt), har jeg ikke kontrol over, hvad VB6 runtime gør i sin interne trådinitialisering og nedrivningsrutiner.


I teorien ville jeg, hvis jeg kunne tvinge VB6 runtime initialiseringskoden til at køre inde OS Loader Lock, forhindre deadlock, fordi jeg er temmelig sikker på, at mutex VB6 runtime holder er kun specifikt brugt indeni initialiserings- og opsigelsesrutinerne.


Krav



  • Jeg kan ikke gøre COM-opkaldene fra en enkelt STA-tråd, for da vil tjenesten ikke kunne klare samtidige anmodninger. Jeg kan ikke have en langvarig forespørgsel blokere også andre klientanmodninger. Det er derfor, jeg opretter en STA-tråd pr. Anmodning.

  • Jeg skal oprette en ny forekomst af COM-objektet på hver tråd, fordi jeg skal sørge for, at hver forekomst har sin egen kopi af globale variabler i VB6-koden (VB6 giver hver tråd sin egen kopi af alle globale variabler) .



Løsninger jeg har prøvet, virkede ikke


Konverteret ActiveX DLL til ActiveX EXE


For det første forsøgte jeg den indlysende løsning og oprettede en ActiveX EXE (out-of-process-server) til håndtering af COM-opkald. I starten kompilerede jeg det således, at der blev oprettet en ny ActiveX EXE (proces) til hver indkommende forespørgsel, og jeg prøvede det også med kompileringsindstillingen Tråd pr. Objekt (en procesinstans er oprettet, og den skaber hvert objekt på en ny tråd i ActiveX EXE).


Dette løser problemet med indgreb med hensyn til VB6 runtime, fordi VB6 runtime aldrig bliver lastet ind i .NET-koden korrekt. Dette førte imidlertid til et andet problem: Hvis samtidige forespørgsler kommer ind i tjenesten, har ActiveX EXE tendens til at mislykkes tilfældigt med RPC\_E\_SERVERFAULT fejl. Jeg går ud fra, at det skyldes, at COM marshalling og/eller VB6 runtime ikke kan behandle samtidig oprettelse/ødelæggelse af genstande eller samtidige metodeopkald inden for ActiveX EXE.


Tving VB6-koden til at køre inde i OS Loader Lock


Derefter skiftede jeg tilbage til at bruge en ActiveX DLL til COM-klassen. For at tvinge VB6 runtime til at køre sin tråd initialiseringskode inde i OS Loader Lock, oprettede jeg en native (Win32) C ++ DLL, med kode til at håndtere DLL\_THREAD\_ATTACH i DllMain . Koden DLL\_THREAD\_ATTACH kalder CoInitialize og instanserer derefter en dummy VB6 klasse for at tvinge VB6 runtime til at blive indlæst og tvinge runtime initialiseringsrutinen til at køre på tråden.


Når Windows-tjenesten starter, bruger jeg LoadLibrary til at indlæse denne C ++ DLL i hukommelse, så alle tråde, der oprettes af tjenesten, udfører den DLL-s DLL\_THREAD\_ATTACH -kode.


Problemet er, at denne kode kører for hver tråd, som tjenesten skaber, herunder .NET affaldssamlerstrengen og trådtrådets tråde, der anvendes af async-netværkskoden, hvilket ikke slutter godt synes at få trådene til aldrig at starte ordentligt, og jeg forestiller mig at initialisere COM på GC og tråd-tråde er generelt kun en meget dårlig idé).



   Tillæg

  
  Jeg har lige indset, hvorfor dette er en dårlig ide (og sandsynligvis en del af grunden til, at det ikke fungerede): det er ikke sikkert at ringe LoadLibrary , når du holder læsserlås. Se afsnittet Anmærkninger i denne MSDN-artikel: http://msdn.microsoft.com/en-us/library/ms682583\%28VS.85\%29.aspx, specifikt: [15]

  
   Tråd i DllMain holder låselåsen, så ingen yderligere DLL'er kan indlæses eller initialiseres dynamisk.



Er der nogen måde at løse disse problemer?


Så mit spørgsmål er, er der nogen måde at arbejde rundt om det originale problem med problemet med?


Det eneste andet, jeg kan tænke på, er at oprette mit eget låsobjekt og omslutte koden, der instanser COM-objektet i en. NET lock blok, men så har jeg ingen måde (som jeg ved) til at sætte samme lås omkring (operativsystemets) tråd udgangskode.


Er der en mere indlysende løsning på dette spørgsmål, eller er jeg helt ude af lykke her?

Bedste reference


Så længe alle dine moduler arbejder i en proces, kan du koble Windows API ved at erstatte nogle systemopkald med dine wrappere. Derefter kan du samle opkaldene i en enkelt kritisk sektion for at undgå dødlås.


Der er flere biblioteker og prøver til at opnå det, teknikken er almindeligvis kendt som omvejning:


http://www.codeproject.com/Articles/30140/API-Hooking-with-MS-Detours[16]


http://research.microsoft.com/en-us/projects/detours/[17]


Og selvfølgelig skal implementeringen af ​​wrappers ske i indbygget kode, helst C ++. .NET omkørsler virker også til API-funktioner på højt niveau som MessageBox , men hvis du forsøger at geninvestere LoadLibrary API-opkald i .NET, kan du få et cyklisk afhængighedsproblem, fordi .NET runtime bruger internt LoadLibrary funktion under udførelse og gør det ofte.


Så ligner løsningen mig sådan: et separat .DLL-modul, som er indlæst i starten af ​​din ansøgning. Modulet løser problemet med indgreb ved at lappe flere VB- og Windows API-opkald med dine egne indpakninger. Alle wrappers gør én ting: Sæt opkaldet i kritisk sektion og påkal den oprindelige API-funktion for at gøre det rigtige job.

Andre referencer 1


EDIT: i bagud, tror jeg ikke, det vil fungere. Problemet er, at dræbningen kan forekomme til enhver tid, som en Win32-tråd udgår, og da Win32-tråde ikke 't map 1: 1 til .NET tråde, kan vi ikke' t '(inden .NET) tvinge Win32-tråde til at erhverve låsen før de forlader. Ud over muligheden for at .NET-tråden, der springer, skiftes til en anden OS-tråd , er der sandsynligvis OS-tråde, der ikke er forbundet med nogen .NET tråd (affaldssamling og lignende), som kan starte og afslutte tilfældigt.



  

    Det eneste andet, jeg kan tænke på, er at skabe min egen låsobjekt
    og omgiver koden, der instanser COM-objektet i en .NET-lås
    blokere, men så har jeg ingen måde (som jeg ved) til at sætte den samme lås
    omkring operativsystemets (s) trådudgangskode.

  

  
  Det lyder som en lovende tilgang. Jeg samler herfra fra dig
  er i stand til at ændre serviceens kode, og du siger hver tråd
  frigiver eksplicit COM-objektet, inden du forlader det, så formodentlig dig
  kunne kræve en lås på dette tidspunkt, enten lige før eksplicit
  frigiver COM-objektet eller lige efter. Hemmeligheden er at vælge en
  type lås, der er implicit frigivet, når tråden holder den
  er ophørt, såsom en Win32 mutex. [18]

  
  Det er sandsynligt, at en Win32 mutex objekt ikke bliver forladt indtil
  tråden har gennemført alle DLL\_THREAD\_DETACH opkald, selv om jeg ikke gør det
  ved, om denne adfærd er dokumenteret. Jeg er ikke bekendt med
  låsning i. NET, men mit gæt er, at de usandsynligt er passende,
  fordi selv om den rigtige slags lås eksisterer, ville det være sandsynligt
  anses forladt, så snart tråden når slutningen af
  administreret kodeafsnit, dvs. før opkaldene til DLL\_THREAD\_DETACH.

  
  Hvis Win32 mutex objekter ikke gør tricket (eller hvis du meget rimeligt
  foretrækker ikke at stole på uokumenteret adfærd), du måtte have brug for
  Gennemfør låsen selv. En måde at gøre dette på ville være at bruge
  OpenThread for at få et håndtag til den aktuelle tråd og gemme dette i din
  lås objekt, sammen med en begivenhed eller lignende objekt. Hvis låsen har
  er blevet hævdet, og du vil vente på, at den er tilgængelig, brug
  WaitForMultipleObjects at vente til enten trådhåndtaget eller
  hændelsen er signaleret. Hvis begivenheden signaleres betyder det, at låsen har
  blevet udtrykkeligt frigivet, hvis trådhåndtaget er signaleret, var det
  implicit frigivet af tråden Naturligvis implementering
  dette indebærer en masse vanskelige detaljer (for eksempel: når en tråd
  frigiver låsen udtrykkeligt, du kan ikke lukke trådhåndtaget
  fordi en anden tråd måske venter på den, så du bliver nødt til at lukke
  det når lås næste gang hævdes i stedet), men det burde ikke være for
  svært at sortere disse ud.


Andre referencer 2


Jeg kan ikke se nogen grund til, hvorfor du ikke kunne indlæse et ekstra eksempel på ActiveX-kontrollen i din startkode og bare hænge på referencen. Presto, ikke flere problemer med låsespærring, da VB6 runtime aldrig lukker ned.

Andre referencer 3


Da jeg stadig undersøger mine muligheder, ville jeg stadig se, om jeg kunne implementere en løsning i ren .NET-kode uden at bruge nogen kode, for enkelhedens skyld. Jeg er ikke sikker på, om dette er en tålsikker løsning men fordi jeg stadig forsøger at finde ud af om det faktisk giver mig den gensidige udelukkelse, jeg har brug for, eller hvis det bare ligner det.


Eventuelle tanker eller kommentarer er velkomne.


Den relevante del af koden er nedenfor. Nogle noter:



  • Metoden HandleRpcRequest kaldes fra en trådspole tråd, når en ny besked er modtaget fra en fjernklient

  • Dette affyrer en separat STA-tråd, så den kan gøre COM-opkaldet sikkert

  • DbRequestProxy er en tynd wrapper klasse omkring den ægte COM klasse I 'm bruger

  • Jeg brugte en ManualResetEvent (\_safeForNewThread) for at give den gensidige udelukkelse. Grundtanken er, at denne begivenhed forbliver usigneret (blokering af andre tråde), hvis en bestemt tråd er ved at afslutte (og dermed potentielt at afslutte VB6 runtime). Begivenheden signaleres kun igen, når den aktuelle tråd slutter helt (efter at Join opkaldet er afsluttet). På denne måde kan flere forespørgselshandler-tråde stadig udføres samtidigt, medmindre en eksisterende tråd forlader.



Hidtil har jeg tænkt denne kode korrekt og garanterer, at to tråde ikke længere kan stoppe i VB6 runtime initialisering/terminationskode længere, mens de stadig giver dem mulighed for at udføre samtidig i det meste af deres gennemførelsestid, men Jeg kunne mangle noget her.


public class ClientHandler {

    private static ManualResetEvent \_safeForNewThread = new ManualResetEvent(true);

    private void HandleRpcRequest(string request)
    {

        Thread rpcThread = new Thread(delegate()
        {
            DbRequestProxy dbRequest = null;

            try
            {
                Thread.BeginThreadAffinity();

                string response = null;

                // Creates a COM object. The VB6 runtime initializes itself here.
                // Other threads can be executing here at the same time without fear
                // of a deadlock, because the VB6 runtime lock is re-entrant.

                dbRequest = new DbRequestProxy();

                // Call the COM object
                response = dbRequest.ProcessDBRequest(request);

                // Send response back to client
                \_messenger.Send(Messages.RpcResponse(response), true);
                }
            catch (Exception ex)
            {
                \_messenger.Send(Messages.Error(ex.ToString()));
            }
            finally
            {
                if (dbRequest != null)
                {
                    // Force release of COM objects and VB6 globals
                    // to prevent a different deadlock scenario with VB6
                    // and the .NET garbage collector/finalizer threads
                    dbRequest.Dispose();
                }

                // Other request threads cannot start right now, because
                // we're exiting this thread, which will detach the VB6 runtime
                // when the underlying native thread exits

                \_safeForNewThread.Reset();
                Thread.EndThreadAffinity();
            }
        });

        // Make sure we can start a new thread (i.e. another thread
        // isn't in the middle of exiting...)

        \_safeForNewThread.WaitOne();

        // Put the thread into an STA, start it up, and wait for
        // it to end. If other requests come in, they'll get picked
        // up by other thread-pool threads, so we won't usually be blocking anyone
        // by doing this (although we are blocking a thread-pool thread, so
        // hopefully we don't block for *too* long).

        rpcThread.SetApartmentState(ApartmentState.STA);
        rpcThread.Start();
        rpcThread.Join();

        // Since we've joined the thread, we know at this point
        // that any DLL\_THREAD\_DETACH notifications have been handled
        // and that the underlying native thread has completely terminated.
        // Hence, other threads can safely be started.

        \_safeForNewThread.Set();

    }
}

Andre referencer 4


Jeg havde skrevet en ret kompliceret kode ved hjælp af VB6, VC6 omkring 20 år siden, og jeg har brug for at sende den til visuel studio.net.
Jeg tog simpelthen de funktioner, som jeg havde skrevet dem sammen med headerfilerne rettet alle kompileringsfejlene (som var mange) og prøvede derefter at indlæse det. fik 'loaderlock lukket'
Jeg besluttede derefter at lave alle de filer, der startede fra dem, som få andre filer var afhængige af, og så arbejdede min vej op, og da jeg gik, inkluderede jeg kun de headerfiler, som den pågældende fil krævede. Resultatet det laster nu lige fint. ingen mere loaderlock lukket.
lektionen for mig er ikke inkluderet flere overskriftsfiler i en bestemt cpp-fil, end det er absolut nødvendigt.
Håber dette hjælper


fra en meget glad camper !!


david