Excel.Application - Der RPC-Server ist nicht verfügbar

  • VB.NET

Es gibt 7 Antworten in diesem Thema. Der letzte Beitrag () ist von hal2000.

    Excel.Application - Der RPC-Server ist nicht verfügbar

    Ich habe ein seltsames Verhalten bei meiner Applikation.

    Für mich sieht es so aus als würde Excel an verschiedenen stellen im Code einfach abstürzen, wenn dann im weiteren Verlauf versucht wird auf Excel zuzugreifen kommt es zum Fehler "Der RPC-Server ist nicht verfügbar".
    Ich starte bei Programmstart eine Excel-Instanz, diese halte ich mir zur gesamten Laufzeit offen. Ein Quit und ReleaseObject mache ich erst wenn ich mein Programm beende.

    Zur Laufzeit habe ich eine Schleife, die verschiedenste Excel-Dateien öffnet ausliest etc. Das seltsame ist, dass die Exception an verschiedenen Stellen auftritt. Ich habe zum Debuggen eine Prozedur geschrieben, die diese Schleife Endlos durchläuft. Das interessante ist, manchmal läuft die Prozedur mehrmals ohne abzustürzen durch, in der Regel kommt es zum Absturz zwischen 2 und bis zu 20 Durchläufen.

    Gibt es irgendwelche bekannten Gründe wieso sich Excel verabschiedet obwohl ich das Objekt nicht freigebe?

    Man kann das ganze auch simulieren indem man während das Programm läuft den Excel-Prozess über den Taskmanager beendet.
    "Es ist absolut möglich, dass jenseits der Wahrnehmung unserer Sinne ungeahnte Welten verborgen sind." — Albert Einstein
    "Wenn die Macht der Liebe die Liebe zur Macht übersteigt, erst dann wird die Welt endlich wissen, was Frieden heißt." — Jimi Hendrix

    Hallo.

    Das häufigste Problem ist, dass du vergisst, temporäre Objekte freizugeben, die du in der Schleife erzeugst und verwirfst. Das bindet Speicher und verhindert ggf. die Erzeugung neuer Objekte --> Absturz. Darüber habe ich schon so oft Beispiele geschrieben, dass ich es hier nicht nochmal wiederholen möchte. Auch wenn du Excel zwischendurch nicht beenden möchtest, gilt der Inhalt dieses Threads trotzdem: [VB 2010] Excel Prozess nach Schreiben in Exceldatei beenden

    Siehe auch:
    social.technet.microsoft.com/w…erver-is-unavailable.aspx
    msdn.microsoft.com/en-us/libra…79805%28office.11%29.aspx
    Gruß
    hal2000
    Sieht so aus als wäre dies das Problem gewesen,

    kurze Frage dazu:
    Ist releaseObject(wb) in Test2 nötig oder reicht es releaseObject(xlWb) in Test?

    VB.NET-Quellcode

    1. Public Sub Test()
    2. Dim xlApp As New Excel.Application
    3. Dim xlWb As Excel.Workbook = xlApp.Workbooks.Open("Path", False, True, )
    4. Test2(xlWb)
    5. xlWb.Save()
    6. xlWb.Close()
    7. releaseObject(xlWb)
    8. releaseObject(xlApp)
    9. End Sub
    10. Public Sub Test2(ByVal wb As Excel.Workbook)
    11. Dim ws As Excel.Worksheet = wb.Worksheets(1)
    12. ws.Activate()
    13. releaseObject(ws)
    14. releaseObject(wb)
    15. End Sub
    16. Public Sub releaseObject(ByVal obj As Object)
    17. Try
    18. System.Runtime.InteropServices.Marshal.ReleaseComObject(obj)
    19. obj = Nothing
    20. Catch ex As Exception
    21. obj = Nothing
    22. Finally
    23. GC.Collect()
    24. End Try
    25. End Sub


    Also generell, reicht es das Objekt ganz am schluss freizugeben oder muss man es in jeder Subroutine freigeben?
    Muss man bei einer For-Each Schleife auch ein release Object machen? Das kostest viel Performance wie ich bemerkt habe.

    VB.NET-Quellcode

    1. For Each xlsName As Excel.Name In Workbook.Names
    2. Dim Val As Object = xlsName.RefersToRange.Value
    3. releaseObject(xlsName)
    4. Next


    Könnte man ja theoretisch durch das hier ersetzten: hier hab ich keine temporäre Variable und kann mir das release sparen oder?

    VB.NET-Quellcode

    1. For i = 1 To Workbook.Names.Count
    2. Dim Val As Object = CType(Workbook.Names(i), Excel.Name).RefersToRange.Value
    3. Next
    "Es ist absolut möglich, dass jenseits der Wahrnehmung unserer Sinne ungeahnte Welten verborgen sind." — Albert Einstein
    "Wenn die Macht der Liebe die Liebe zur Macht übersteigt, erst dann wird die Welt endlich wissen, was Frieden heißt." — Jimi Hendrix

    Ich habe deine Posts mal zusammengefügt.

    Allgemein: Lass die Methode releaseObject weg. Das erzeugt beim Aufruf einen impliziten Cast nach Object und hat zur Folge, dass das Framework den Cast von COM (vom Excel-Objekt) anfordert. Das kostet Performance (2 Marshallingvorgänge - 1x hin, 1x zurück). Außerdem rufst du jedes Mal GC.Collect() auf - das ist unschön (heißt: Lass das). Das ist auch der Grund, warum deine For-Schleife so langsam ist. Der GC macht das von alleine, wenn er es für sinnvoll erachtet. Tatsächlich findet er sogar fast immer den besseren Zeitpunkt als du. Eine Lösung für eine kurze Schreibweise:

    VB.NET-Quellcode

    1. Imports M = System.Runtime.InteropServices.Marshal
    2. M.ReleaseComObject(...)


    Zu Test2: In Zeile 12 darf das wb-Objekt nicht freigegeben werden, denn du möchtest es in Zeile 5 und 6 (nach der Rückkehr aus Test2) noch verwenden. Die Antwort ist also "Ja, es reicht in Test". Du verlierst aber noch immer eine Referenz in Zeile 3 (auf xlApp.Workbooks). Besser:

    VB.NET-Quellcode

    1. Dim xlApp As New Excel.Application
    2. Dim xlWbs As Excel.Workbooks = xlApp.Workbooks 'Workbooks ist eine Collection (und damit ein freizugebendes Objekt)
    3. Dim xlWb As Excel.Workbook = xlWbs.Open("Path", False, True, )
    4. Test2(xlWb)
    5. 'Save, Close
    6. M.ReleaseComObject(xlWb)
    7. 'weiteres Workbook öffnen, benutzen, speichern, schließen und freigeben
    8. M.ReleaseComObject(xlWbs)
    9. M.ReleaseComObject(xlApp)


    Zu deiner Schleife: Hier verlierst du gleich ganz viele Referenzen: In jeder Iteration eine auf ein Range-Objekt (aus xlsName.RefersToRange) und in der Schleife selbst auf die Collection aus Workbook.Names. Besser:

    VB.NET-Quellcode

    1. Dim names As Excel.Names = Workbook.Names
    2. 'Keine For-Each-Schleifen benutzen
    3. For i As Int32 = 0 To names.Count - 1
    4. Dim valRange As Excel.Range = names(i).RefersToRange
    5. Dim Val As Object = valRange.Value
    6. 'mit Val arbeiten
    7. If M.IsComObject(Val) Then 'kann man weglassen, wenn Val immer ein primitiver Typ ist
    8. M.ReleaseComObject(Val)
    9. End If
    10. M.ReleaseComObject(valRange)
    11. Next

    Faustregel: Keine Aufrufketten verwenden, die mehr als einen Punkt beinhalten. Die Excel-Interop-Dokumentation sagt dir, welche Aufrufe Objekte zurückliefern. Alles was irgendwie nach "komplexer Typ" klingt, muss freigegeben werden.
    Gruß
    hal2000
    Danke für die Antwort :)

    Das ist echt bescheiden :-/ ich dachte ich könnte es vielleicht übergehen mit diesen Aufrufketten, da ich ja keine Variable deklariere.

    Mit entsetzen muss ich feststellen, dass eine For i=0 To Next Schleife um Faktor 10 langsamer ist wie in einer For Each Schleife :/ liegt wohl am Zugriff über den Index

    Wäre das keine saubere Lösung?

    VB.NET-Quellcode

    1. Dim xlNames As Excel.Names = Workbook.Names
    2. For Each xlName As Excel.Name In xlNames
    3. Dim xlRange As Excel.Range = xlName.RefersToRange
    4. Dim Val As Object = xlRange.Value
    5. System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRange)
    6. System.Runtime.InteropServices.Marshal.ReleaseComObject(xlName)
    7. Next
    8. System.Runtime.InteropServices.Marshal.ReleaseComObject(xlNames)


    statt

    VB.NET-Quellcode

    1. Dim xlNames As Excel.Names = Workbook.Names
    2. For i = 1 To xlNames.Count - 1
    3. Dim xlName As Excel.Name = CType(xlNames(i), Excel.Name)
    4. Dim xlRange As Excel.Range = xlName.RefersToRange
    5. Dim Val As Object = xlRange.Value
    6. System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRange)
    7. System.Runtime.InteropServices.Marshal.ReleaseComObject(xlName)
    8. Next
    9. System.Runtime.InteropServices.Marshal.ReleaseComObject(xlNames)


    Wäre das keine gleichwertige Lösung? Der Zugriff über den Index kostet viel Performance
    "Es ist absolut möglich, dass jenseits der Wahrnehmung unserer Sinne ungeahnte Welten verborgen sind." — Albert Einstein
    "Wenn die Macht der Liebe die Liebe zur Macht übersteigt, erst dann wird die Welt endlich wissen, was Frieden heißt." — Jimi Hendrix

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „FredM“ ()

    FredM schrieb:

    Wäre das keine saubere Lösung?
    Doch, das könnte funktionieren, weil du jedes enumerierte Objekt freigibst. Wenn du sicher gehen möchtest, kannst du GetEnumerator() aufrufen und den Enumerator in einer While-Schleife abarbeiten. Das ist quasi die For-Each-Schleife im manuellen Modus.

    Das ganze Excel-Interop-Kram ist sowieso insgesamt langsam, weil die Daten durch mehrere Subsysteme fließen. Jeder Aufruf muss zuerst über die Marshalling-Schnittstelle des Frameworks, damit das unverwaltete COM damit arbeiten kann. Weiter gehts über einen RPC an Excel - wieder mit Marshalling, weil eine Prozessgrenze dazwischen liegt. RPCs über Prozessgrenzen erfordern Serialisierung von Parametern, was ebenfalls dauert. Das Problem liegt also darin, dass die Daten mehrfach kopiert werden und außerdem warten müssen, bis der Zielprozess (hier Excel) überhaupt für einen RPC verfügbar ist.

    Mögliche Lösung: Verarbeite Daten nicht einzeln, sondern in größeren Blöcken. Soll heißen: Übertrage mehr als eine Zelle gleichzeitig und gehe die Liste / Tabelle lokal durch. Du kannst Excel-Tabellen auch mit SQL abfragen - das ist ein Quantensprung in der Performance, weil Excel die Daten direkt analysiert und in einem Rutsch nur das zurückgibt, was du willst. Noch besser ist es, wenn du deine Tabellen als CSV-Dateien speicherst und direkt in dein Programm einliest. Damit vermeidest du eine Menge Probleme.
    Gruß
    hal2000

    hal2000 schrieb:

    FredM schrieb:

    Wäre das keine saubere Lösung?
    Doch, das könnte funktionieren, weil du jedes enumerierte Objekt freigibst. Wenn du sicher gehen möchtest, kannst du GetEnumerator() aufrufen und den Enumerator in einer While-Schleife abarbeiten. Das ist quasi die For-Each-Schleife im manuellen Modus.


    Danke hal :)

    Das mit den Blöcken mache ich bereits, wenn ich nur Daten einlesen müsste würde ich ganz auf Excel verzichten, das Prinzip des Programmes ist, dass es eine Art Schnittstelle bildet Zwischen Excel und Daten. Es werden beim öffnen einer Mappe Daten aus dem Programm an Excel übergeben, der User macht die Berechnungen in Excel und anschließend werden die Werte zurückgelesen.

    :?: Frage, geh ich richtig in der Annahme, wenn ich folgenden Code ausführe und anschließend Excel aus dem Taskmanager verschwindet hab ich alle Objekte richtig freigegeben. Bleibt der Prozess offen gibt es noch irgendwo eine nicht freigegebene Referenz.?

    VB.NET-Quellcode

    1. Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click
    2. ExcelApp.Quit()
    3. System.Runtime.InteropServices.Marshal.FinalReleaseComObject(ExcelApp)
    4. End Sub


    Hier in diesem Thread steht auch ziemlich viel zu diesem Thema. stackoverflow.com/questions/15…-up-excel-interop-objects

    Spielt es eigentlich eine Rolle ob ich FinalReleaseComObject oder ReleaseComObject verwende? Irgendwie schreibt da jeder bisschen was anderes über das Thema. :S
    "Es ist absolut möglich, dass jenseits der Wahrnehmung unserer Sinne ungeahnte Welten verborgen sind." — Albert Einstein
    "Wenn die Macht der Liebe die Liebe zur Macht übersteigt, erst dann wird die Welt endlich wissen, was Frieden heißt." — Jimi Hendrix

    FredM schrieb:

    Frage, geh ich richtig in der Annahme, wenn ich folgenden Code ausführe und anschließend Excel aus dem Taskmanager verschwindet hab ich alle Objekte richtig freigegeben. Bleibt der Prozess offen gibt es noch irgendwo eine nicht freigegebene Referenz.?
    In COM gibt es keinen Garbage Collector wie in .NET. Weil man aber trotzdem irgendwie wissen muss, wann Objekte freigegeben werden können, verwaltet jedes Objekt für sich selbst einen Referenzzähler. Jede neue Kopie erhöht den Zähler um 1, jedes Marshal.ReleaseComObject() verringert ihn um 1 und gibt dann den neuen Zählerwert zurück. Marshal.FinalReleaseComObject() setzt den Zähler einfach auf 0, was m.E. nicht ganz sauber ist. Du gibst also letztendlich etwas frei, von dem du nicht weißt, wann und wo du es angefordert hast.

    Im Prinzip ist deine Aussage richtig - nur würde ich Marshal.ReleaseComObject (ohne Final) verwenden. Wenn dort 0 zurückkommt, hast du keine Referenzen vergessen und auch der Excel-Prozess beendet sich. Beachte aber, dass der Garbage Collector manchmal etwas behäbig ist - es kann sein, dass der Excel-Prozess nicht nicht endet, weil du was vergessen hast, sondern weil der GC noch nicht gelaufen ist. Auch dafür hatte ich mal ein Beispiel geschrieben: [VB 2010] Excel Prozess nach Schreiben in Exceldatei beenden (im Expander in Post 7 versteckt).
    Das MSDN sagt dazu:
    However, the RCW can still exist, waiting to be garbage-collected.
    Gruß
    hal2000