Parallelisierung mit Prozessen statt Threads
-
loks schrieb:
Das hört sich mehr nach COM an (Component Object Model). Das ist zwar alles andere als Trivial, aber es ist explizit darauf ausgelegt über Processgrenzen hinweg zu arbeiten. Hier sind Themen wir Marshalling komplett fertig vorhanden.
Und wo wäre der Vorteil gegenüber NET-Remoting oder WCF?
Es ist ja i.d.R. kein Kunstwerk Typen für Remoting serialisierbar zu machen, der Rest ist Konfiguration der Verbindung und das wars.
-
hustbaer schrieb:
Es geht bei deiner Fragestellung aber nur um technische Details.
Eigentlich nicht. Die Antworten deuten darauf hin, daß mein Problem nicht verstanden wurde und ich mich deutlicher ausdrücken muß. (MPI ist nett, aber es ist nur ein low-level-Detail.)
hustbaer schrieb:
z.B. kannst du serialisierbare Work-Items basteln, und diese dann per .NET Remoting oder sonst einen IPC-Mechanismus an die Worker-Prozesse versenden.
Jaja, das passiert backstage, das bekomme ich hin und darum gehts nicht.
hustbaer schrieb:
Das geht aber natürlich nur wenn du eben serialisierbare Work-Items basteln kannst. Was von vielen technischen Details deiner Anwendung abhängt -- die wir nicht wissen können.
Ich versuche es besser darzulegen:
Ich habe eine Sammlung von Bildauswertungsroutinen aus dem wissenschaftlichen Bereich, teils in Python, teils in MATLAB geschrieben. Der gegenwärtige Zustand ist, daß diese unzusammenhängend sind und manuell bedient werden müssen (manche mit GUI, manche aus MATLAB), und daß sie alle natürlich nur single-threaded laufen, weil das in MATLAB und in Python eben so ist.
Ich möchte nun die Leute, die diese Skripte warten und benutzen, dazu bringen, den Prozeßablauf in einem C#-"Skript" niederschreiben zu können.1 Idealerweise ermögliche ich damit nicht nur die Automatisierung des gesamten Ablaufes, sondern auch die Parallelausführung. Aus den genannten Gründen kann diese nicht in mehreren Threads stattfinden, sondern erfordert mehrere Prozesse.
Die Interfaces nach MATLAB und Python existieren bereits. Weil die Leute es gewohnt sind, Arbeitsabläufe prozedural niederzuschreiben, wäre es gut, wenn mein Skript das bis zu einem gewissen Grad unterstützt. LINQ ist dann natürlich auch eine unbekannte Neuerung, aber der ästhetische Vorteil gegenüber äquivalentem prozeduralen Code ist so immens, daß ich gewisse Hoffnungen habe, es den Leuten angewöhnen zu können. Aber manchmal will man eine Berechnung halt doch mit einer for-Schleife machen: und meine Frage ist, ob es sinnvolle und praktikable Möglichkeiten gibt, eine Schnittstelle analog zu
Parallel.For()
zu entwickeln, die ein Delegate entgegennimmt, das Delegate in einem anderen Prozeß ausführen läßt und die Ergebnisse wieder in den Parent-Prozeß zurückführt.Die Ansätze, die mir dazu so einfallen, sind:
- ich könnte versuchen, den Delegate mittels Reflection zu serialisieren. Das würde sicher viel Handarbeit erfordern, da ich alle erfaßten (captured) Variablen (die, wenn ich nicht falsch liege, von der CLR als Felder einer anonymen Klasse implementiert werden, deren "Invoke"-Methode der Delegate-Body ist) qualitativ untersuchen und entscheiden müßte, wie diese zu serialisieren sind. Das ist speziell dann schwierig, wenn der Body wahllos in globalen Arrays rumschreibt; und sowas kann ich mit Reflection nicht herausfinden.
- ich könnte NRefactory benutzen, um die erfassten Variablen des Delegates schon im Voraus zu kennen und evtl. gewisse Praktiken (solche, die eine andauernde Synchronisation erfordern: wahlfreier Schreibzugriff auf ein globales Array) mit einem Fehler zu quittieren, der nicht erst zur Laufzeit entsteht. Oder wenn Schreibzugriffe auf Arrays erlaubt sein sollen (ist ja was recht gewöhnliches:
for (i = 1 to 10) result[i] = myFunc(i);
, kann ich mit NRefactory das Array durch eine spezielle Klasse ersetzen, die die Schreibzugriffe in den jeweiligen Prozessen ausführt, sich merkt, welcher Index von welchem Prozeß geschrieben wurde und das Array hinterher zusammenführt; in diesem Fall sollte ich aber wahlfreie Lesezugriffe auf das Array unterbinden (da nicht deterministisch). Usw.Und das sind für mich durchaus konzeptuelle Probleme. Die technischen Details (wie mache ich das mit NRefactory, wie serialisiere ich die Delegate-Klasse) sind im Moment nicht das Problem; ich möchte nur sichergehen, daß ich nicht in ein SNAFU reinlaufe, weil ich ein grundsätzliches Problem übersehen habe.
1Warum C# und nicht irgendeine Exotensprache, die für das Problem wie geschaffen ist?
- weil die Einbindung von Drittcode in .NET sehr leicht ist (-> Reflection, Codegeneration zur Laufzeit)
- weil C# für Leute, die mit <insert any common scientific language> arbeiten, sehr leicht verständlich und erlernbar ist
- weil ich Java-Libraries mit IKVM zu .NET-Libraries machen kann und dann eine Laufzeitumgebung weniger brauche
- weil ich C#-Code mit NRefactory nach Belieben analysieren und manipulieren kann (für spätere Erweiterung des Projekts wichtig)
- Visual Studio
- LINQ
-
Ich glaube dass du mit der Idee die Lambdas zu serialisieren und instrumentieren eine Welt des Schmerzes aufmachst. Und natürlich eine Welt der schlechten Performance.
Klar, theoretisch mag da viel möglich sein, aber ich halte es nicht für praktikabel. U.a. schon deswegen, weil du es nicht hinbekommen wirst alles einfach so automatisch serialisieren zu lassen. Gerade wenn nicht-.NET Libraries im Spiel sind.
Ich würde das ganze anders angehen - und zwar einen Schritt vor dem "parallel for" Loop ansetzen.
Sieh zu, dass alles mit dem deine "Process" Funktion, die den "parallel for" Loop enthält, aufgerufen wird, serialisierbar ist.
Diese Daten + die Info welche "Partition" sie bearbeiten sollen verteilst du dann an die Prozesse. In der "Process" Funktion wird dann in jedem Prozess das selbe Lambda aufgebaut und mit den für die Partition passenden Werten von deinem "parallel for" Konstrukt aufgerufen.Dann können die Prozesse alle fröhlich vor sich hin werken, und wenn alle fertig sind musst du "nur" noch die Ergebnisdaten zusammenmergen.
Und das sollte mMn. recht einfach gehen. Zumindest wenn die "parallel for Lambdas" derart sind wie man es bei "parallel for" haben möchte, nämlich dass keiner was schreibt was ein anderer auch schreibt und keiner von dem abhängig ist (liest) was ein anderer geschrieben hat.
Dann musst du nämlich nur rausbekommen welche Werte von welchem Prozess modifiziert wurden, und alles passend zusammenmergen.Also angenommen du willst einen Blur-Filter parallelisieren, dann werden die Lambdas halt ihre geblurten Pixel in irgend einen Frame-Buffer reinschreiben. Und zwar so dass jeder Pixel nur von genau einem "Worker" (Thread/Prozess/...) modifiziert wurde -- oder halt von keinem.
Sowas sollte trivial abzudecken sein -- alle Prozesse fangen z.B. mit einem rein schwarzen Frame-Buffer an, und beim Zusammenmergen kopierst du einfach alle Pixel die nicht schwarz sind in den Accumulation-Buffer. Der fängt natürlich auch schwarz an, der Fall wo ein Prozess einen schwarzen Pixel geschrieben hat ist also auch kein Problem, weil der Pixel im Accumulation-Buffer dann zwar nie überschrieben wird, aber von Anfang an schon Schwarz war, also am Ende trotzdem den passenden Wert hat.Das Selbe lässt sich natürlich auch mit anderen Arrays machen. Ein Array von Foo Objekten? Dann ist Schwarz eben null.
In Fällen wo die einzelnen Lambdas doch miteinander kommunizieren müssen können die selben Massnahmen helfen die man auch zur Optimierung von normalen SMP "parallel for" Geschichten verwendet. Also alle Techniken die die "globalen" Variablen durch lokale ersetzen und dann erst am Ende "zusammenzählen". Einfachster Fall Accumulator: jeder Prozess hat seinen eigenen, und am Ende werden nochmal die Werte von allen Prozessen zusammengezählt.
Dazu kannst du Hilfsklassen bauen die diesen Job übernehmen, und deinem "parallel for" Konstrukt bekannt sind.
Also z.B. die oben erwähnten "jedes Element wird nur von einem Worker bearbeitet" Arrays die das System dann automatisch zusammenmergen kann, eine spezielle Accumulation-Buffer Klasse für "einfach nur zusammenzählen", eine "Accumulation-List" für Listen wo jeder Worker Elemente reinhängen kann etc.
-
Oder gehen wir das ganze mal anders an...
Kannst du einen Anwendungsfall für den das ganze funktionieren soll beschreiben, der ausreichend einfach ist dass man ihn hier analysieren kann, aber ausreichend komplex um representativ zu sein. (Also kein vereinfachtes Bilderbuch-Beispiel, das man auf 1001 Wegen lösen kann, die dann aber alle nicht auf das echte Problem übertragbar sind.)
Erstmal würde ich dann vermutlich besser verstehen welche Art von Vorgängen du überhaupt parallelisieren willst. Dann könnte man das Beispiel durchgehen, und alle "Problemstellen" auflisten die es mit deiner Idee (Code analysieren + instrumentieren) geben wird. Und parallel nach "praktischen" Alternativen suchen, und was die alle für potentielle Vor- und Nachteile haben.
-
Bzw. nochmal etwas konkreter...
audacia schrieb:
- ich könnte NRefactory benutzen, um die erfassten Variablen des Delegates schon im Voraus zu kennen und evtl. gewisse Praktiken (solche, die eine andauernde Synchronisation erfordern: wahlfreier Schreibzugriff auf ein globales Array) mit einem Fehler zu quittieren, der nicht erst zur Laufzeit entsteht. Oder wenn Schreibzugriffe auf Arrays erlaubt sein sollen (ist ja was recht gewöhnliches:
for (i = 1 to 10) result[i] = myFunc(i);
, kann ich mit NRefactory das Array durch eine spezielle Klasse ersetzen, die die Schreibzugriffe in den jeweiligen Prozessen ausführt, sich merkt, welcher Index von welchem Prozeß geschrieben wurde und das Array hinterher zusammenführt; in diesem Fall sollte ich aber wahlfreie Lesezugriffe auf das Array unterbinden (da nicht deterministisch). Usw.Das ist ein Fall, den man vermutlich noch halbwegs einfach erschlagen kann. Aber was machst du wenn as kein Array aus trivialen Integers o.ä. mehr ist, sondern ein Array das auf irgendwelche Objekte zeigt, die intern nen Haufen Referenzen haben die wieder auf weitere Objekte verweisen?
Und ein Thread/Prozess machtarr[i].GetFoo().GetBar().X = 123
und ein anderer machtarr[i].GetFoo().GetBar().Y = 42
. Was in einem "parallel for" ja durchaus OK ist. Das mit einem automatischen Code-Umbastler zu erschlagen erscheint mir ziemlich schwierig. Möglich, aber schwierig. Und alle Varianten die mir so einfallen haben einiges an Overhead.
Am besten ginge sowas vermutlich noch mit einem modifizierten JITer der den entsprechenden Bookkeeping-Code mit erzeugt.Je nachdem wie gross/wichtig das Projekt ist, und wie viel Erfahrung du mit sowas schon hast, mag das praktikabel sein.
Wenn ich es von meinem Standpunkt aus beurteile (=mein Erfahrungsstand mit solchen Dingen), und von nicht megamässig riesigen Dimensionen (Projektlaufzeit, -Budget) ausgehe, dann wäre es sicher nicht praktikabel.
Hiesse für mich: ich müsste hier sehr strikte Limits setzen, und diese idealerweise natürlich auch im Code forcieren (Fehler generieren wenn etwas gemacht wird was das Ding nicht unterstützt - was auch wieder Arbeit ist).
Und ich schätze dass dabei im Endeffekt so wenig sinnvoll verwendbare & "erlaubte" Konstrukte übrig bleiben werden, dass man dafür genau so gut eigene Klassen schreiben kann. Die dann den nötigen Bookkeeping-Code enthalten bzw. mit dem in meinem früheren Beitrag erwähnten "auto merging" Mechanismus unterstützen.Dann muss man den Usern der Library nur klar machen, dass Output generell nur über diese Klassen gemacht werden kann, und alles was man in andere Klassen reinschreibt nach dem "parallel for" einfach "weg" ist.
-
audacia schrieb:
Ich habe eine Sammlung von Bildauswertungsroutinen aus dem wissenschaftlichen Bereich, teils in Python, teils in MATLAB geschrieben. Der gegenwärtige Zustand ist, daß diese unzusammenhängend sind und manuell bedient werden müssen (manche mit GUI, manche aus MATLAB), und daß sie alle natürlich nur single-threaded laufen, weil das in MATLAB und in Python eben so ist.
Ich möchte nun die Leute, die diese Skripte warten und benutzen, dazu bringen, den Prozeßablauf in einem C#-"Skript" niederschreiben zu können.1 Idealerweise ermögliche ich damit nicht nur die Automatisierung des gesamten Ablaufes, sondern auch die Parallelausführung. Aus den genannten Gründen kann diese nicht in mehreren Threads stattfinden, sondern erfordert mehrere Prozesse.
Ich bin noch nicht ganz dahinter gestiegen, was genau Du parallelisieren möchtest. Du hast also Routinen in Pyhton und MATLAB und kannst die von C# aus aufrufen. Wenn ich es richtig verstehe, möchtest Du mehrere solcher Routinen parallel ausführen, nicht aber einzelne Routinen selbst (Sonst müsstest Du ja auf Pyhton/Matlab-Seite parallelisieren). Richtig?
Arbeiten diese Routinen dann auf unterschiedlichen Eingabebildern, so eine Art Batchprocessing bisher? Das möchtest Du in einem Parallel.For zerteilen?
Ansonsten hast Du doch eine feste Sequenz von Operationen und kannst nicht mit der nächsten Anfang bevor das Ergebnis der vorherigen da ist.Kannst Du vielleicht mal ein paar Beispiele solcher Routinen nennen und wie so ein C#-"Skript" aussehen würde?
-
Ist das mit dem Script vielleicht so gemeint ?
private Task<Foo> TuWasInMatlab(Bar parameters) { return Task.Run( () => { // Einen Matlab-Prozess starten und auf Ergebnis warten }); } private Task<Foo> TuWasInPhyton(Bar parameters) { // s.o. } // Das eigentliche "Script" public async RunScript() { var a = TuWasInMatlab(...); var b = TuWasInPyhton(...); var c = TuWasInMatlab(...); Foo[] Ergebnisse = await Task.WhenAll( a, b, c ); // Jetzt weiter mit LINQ }
-
Danke für das umfangreiche Feedback! Ich will versuchen, auf alle aufgeworfenen Fragen eine kurze Antwort zu geben.
Das C#-Skript ist nicht dazu gedacht, irgendwelche Filter oder so zu implementieren. Es soll nur eine Vorgangsbeschreibung sein; die ganzen Algorithmen liegen schon in MATLAB/Python/C++/Java vor, und das C#-Skript soll daraus einen geschlossenen Arbeitsablauf mit wohldefiniertem Input und Output machen. Da grundsätzlich große Mengen an Eingabedaten anfallen werden, habe ich festgelegt, daß keine Anstrengung darauf verwendet wird, die einzelnen Algorithmen zu parallelisieren; vielmehr will ich Teilvorgänge, die in C# beschrieben sind und z.B. für jedes Eingabebild eine Auswertungsroutine aufrufen, parallel ausführen. Der Gründe sind einige: die Parallelisierung von Algorithmen ist oft sehr aufwendig, skaliert nicht gut und erfordert im Falle von MATLAB unbedingt die Parallel Toolbox. Außerdem würde es massive Eingriffe in bestehenden Fremdcode erfordern, für deren Wartung die Ressourcen fehlen. Dementgegen skaliert eine Parallelisierung auf hoher Ebene (quasi paralleles Batch-Processing) viel besser und ist einfacher umzusetzen.
Was hustbaer in bezug auf die Ergebnisakkumulation beim Blur-Filter beschreibt, ist etwa das, was ich mir bei obigen Ausführungen zu Schreibzugriffen auf Arrays vorgestellt habe. Den Problemen mit
arr[j].GetFoo().GetBar().Y = 42
könnte ich wahrscheinlich aus dem Weg gehen, wenn meine Referenztypen immutable sind.Zum Akkumulationsbeispiel fällt mir ein, daß der Ansatz von LINQ auch hier sehr gut paßt: wenn ich einen eigenen QueryProvider habe, kann ich auch die ganzen Akkumulationsfunktionen wie
Sum()
oderAny()
selbst für einen ProcessParallelProvider überladen. D.h., solange ich nur geeignete LINQ-Statements parallelisiere, wäre die Angelegenheit relativ schmerzlos zu implementieren: ich bekomme einen Expression-Tree anstelle von kompiliertem Code, ich kann alle möglichen LINQ-Funktionen für meinen Fall geeignet spezialisieren und ich muß mich nicht mit Nebeneffekten wie den globalen Arrays rumschlagen (weil man sowas in LINQ nicht macht... hoffentlich).
Ein realweltnahes Beispiel habe ich auch. Daran wird vielleicht deutlich, daß es zwar aussieht wie Batch-Processing, aber daß es zugleich so einfach nicht ist: ich habe nicht N Inputs und M Outputs, sondern aus einem Input erzeuge ich Teildatensätze, mache Postprocessing (wofür ich wieder eine Akkumulation der Teilbilder brauche), mache weitere Auswertungen mit den Teilbildern, setze sie am Ende wieder zusammen und gebe mein Analyseergebnis dazu.
Ungefähr so würde das einer meiner Kollegen aufschreiben:
struct Result { Image correctedImage; ImageAttributes attributes; } Result process (Image input) { Result result; // [1] Wir bekommen ein großes Bild, das aber aus lauter Einzelaufnahmen besteht, die // von der Mikroskopsoftware zusammengehängt werden. Das Bild teilen wir jetzt wieder auf. Image[] manySubImages = MATLAB.SplitImageInTiles (input); // [2] Zur Verbesserung des Stitchings führen wir optische Korrekturen durch. // Einfaches Beispiel: wir rechnen das Durchschnittsbild aus (das optisch bedingte // Helligkeitsunterschiede aufweist) und subtrahieren das. Image totalSum = manySubImages[0]; for (int i = 1; i < manySubImages.Length; ++i) totalSum = totalSum.Add (img); Image average = totalSum.Divide (manySubImages.Length); Image backgroundCorrection = average.Subtract (average.AveragePixelValue ()); Image[] correctedSubImages = new Image[manySubImages.Length]; for (int i = 0; i < manySubImages.Length; ++i) correctedSubImages[i] = manySubImages[i].Subtract (backgroundCorrection); // [3] Jetzt können wir die einzelnen Bilder auswerten. PythonImageAttributes[] pythonAttributes = new PythonImageAttributes[manySubImages.Length]; MATLABImageAttributes[] matlabAttributes = new MATLABImageAttributes[manySubImages.Length]; for (int i = 0; i < manySubImages.Length; ++i) { pythonAttributes[i] = Python.AnalyzeImage (correctedSubImages[i]); matlabAttributes[i] = MATLAB.AnalyzeImage (correctedSubImages[i]); } // [4] Das große Bild stückeln wir wieder zusammen. result.correctedImage = MATLAB.StitchImages (manySubImages); // [5] Die Ergebnisse aus Python werden in MATLAB post-processed, wobei auf die einzelnen // Bilddaten zurückgegriffen werden muß: ImageAttributes[] attributes = new ImageAttributes[manySubImages.Length]; for (int i = 0; i < manySubImages.Length; ++i) attributes[i] = MATLAB.PostProcess (pythonAttributes[i], matlabAttributes[i], correctedSubImages[i]); // [6] Schließlich die Zusammenfassung: result.attributes = MATLAB.SummarizeResults (attributes); return result; }
Von den numerierten Teilprozessen lassen sich [2], [3] und [5] im Skript parallelisieren. ([4] möglicherweise auch, das hängt davon ab, was genau beim Stitching geschieht [Überlappungen entfernen?] und ist jetzt nicht so wichtig.) [2] müßte man umformulieren, so daß aus der etwas plumpen Summation ein Map-Reduce wird, aber [3] und [5] können ohne weiteres parallelisiert werden, womit bereits viel gewonnen wäre.
Nun hat hustbaer natürlich recht mit dem Kosten/Nutzen-Einwand. Meine Ressourcen sind begrenzt, und ich würde an dieser Stelle lieber darauf verzichten, NRefactory zu bemühen, IL-Code zu parsen und Delegates zu serialisieren. Ideal wäre es, wenn ich die Leute zu funktionalem Code erziehen könnte; aber aufgrund der Personalfluktuationen wäre das eine Sysiphusarbeit
Ich wollte nun eben Überlegungen anstellen, wie ich ihnen mit so wenig Umerziehung wie möglich erlauben kann, ihren Code zu parallelisieren.
Vielleicht sollte ich mich auf eine ganz üble Variante mit Reflection beschränken. Sowas:
// [3] Jetzt können wir die einzelnen Bilder auswerten. PythonImageAttributes[] pythonAttributes = (PythonImageAttributes[]) Multiprocess.ForEach ( function: "Python.AnalyzeImage", parameters: correctedSubImages);
Dann ist halt die statische Typsicherheit weg, Refactoring geht nicht mehr richtig und ich fühle mich wie in der Steinzeit
Aber eine kosteneffektive Lösung wäre es...
-
audacia schrieb:
Vielleicht sollte ich mich auf eine ganz üble Variante mit Reflection beschränken. Sowas:
// [3] Jetzt können wir die einzelnen Bilder auswerten. PythonImageAttributes[] pythonAttributes = (PythonImageAttributes[]) Multiprocess.ForEach ( function: "Python.AnalyzeImage", parameters: correctedSubImages);
Dann ist halt die statische Typsicherheit weg, Refactoring geht nicht mehr richtig und ich fühle mich wie in der Steinzeit
Aber eine kosteneffektive Lösung wäre es...
Das kann man mit relativ wenig Aufwand auch schöner machen
Du könntest z.B. alle Operationen die von einem Worker-Prozess durchgeführt werden sollen in ein IWorkerProcess Interface packen.
Die "for each" Methode würde dann erstmal "ganz normal" auf normale Threads aufteilen, wobei sie für jeden Thread ein IWorkerProcess Interface besorgt, das (z.B. über .NET Remoting - da kann man ja sehr schön Interfaces marshallen) mit dem Worker-Prozess für den jeweiligen Thread verbunden ist. Und dieses Interface an den Lambda übergeben.
Dann könntest du es so schreiben:
var pythonAttributes = Multiprocess.Map( correctedSubImages, (workerProcess, subImage) => workerProcess.Python_AnalyzeImage(subImage));
Das wäre dann zumindest type-safe und Refactoring-freundlich. Und eigentlich auch ganz schön zu lesen.
Oder etwas generischer formuliert: verschieb den RPC/IPC Call von der "for each" Methode ins Lambda.
-
hustbaer schrieb:
Dann könntest du es so schreiben:
var pythonAttributes = Multiprocess.Map( correctedSubImages, (workerProcess, subImage) => workerProcess.Python_AnalyzeImage(subImage));
Das wäre dann zumindest type-safe und Refactoring-freundlich. Und eigentlich auch ganz schön zu lesen.
Das stimmt, aber die Sache mit den Worker-Interfaces finde ich recht einschränkend. Es ist halt wieder so eine unoffensichtliche Beschränkung, die lustige Laufzeitfehler zeitigt oder (eher noch) den Code nur seriell ausführt, wenn man mal eine nicht explizit durch Existenz eines Worker-Interfaces "unterstützte" Funktion aufruft.
Aber wenn ich den Leuten eh schon funktionale Schreibweise beibringen muß, kann ich doch auch gleich versuchen, LINQ durchzusetzen:
var pythonAttributes = from image in correctedSubImages.AsProcessParallel() select Python.AnalyzeImage (image);
Vielleicht fange ich da mal an und schaue, ob's angenommen wird.
-
Dabei wirst du halt wieder das Problem haben Delegates serialisieren zu müssen.
Aber du kannst natürlich einfach mal probiere - sehen wie weit du damit kommst.audacia schrieb:
Es ist halt wieder so eine unoffensichtliche Beschränkung, die lustige Laufzeitfehler zeitigt oder (eher noch) den Code nur seriell ausführt, wenn man mal eine nicht explizit durch Existenz eines Worker-Interfaces "unterstützte" Funktion aufruft.
Finde ich nicht so unoffensichtlich.
Die Leute müssen halt wissen dass diverse Klassen (in deinem Beispiel die MATLAB und Python Klassen) nicht multithreaded verwendet werden können, und sie, wenn es doch parallel laufen soll, die entsprechenden Funktionen aus dem IWorkerProcess Interface verwenden müssen.
Andere Dinge, die keine solche Limitierung haben, können sie ja ohne IWorkerProcess verwenden.Ich fände die Variante auf jeden Fall "logischer", als wenn das "auf Prozesse verteil-Ding" das hinter deinem LINQ Call läuft nur bestimmte Dinge unterstützt, weil das Serialisieren von Delegates nur beschränkt funktioniert.
Also...
Pro IWorkerProcess: Der RPC/IPC Call ist deutlich sichtbar => einfacher zu verstehen.
Pro IWorkerProcess: Jeder RPC/IPC Call der im Interface vorhanden ist funktioniert auch, weil nur Calls mit Parameters/Returntypen die problemlos serialisierbar sind überhaupt angeboten werden.
Pro IWorkerProcess: Der Lambda selbst läuft noch im "Main-Prozess", d.h. man kann auch freizügig diverse lokalen Variablen capturen, aus dem Lambda in shared Arrays/Files/... schreiben bzw. daraus lesen etc.
Pro "automatische" Variante: Man spart sich das Schreiben von Wrapper-Funktionen wenn neue Python/MATLAB/... Funktionen benötigt werden.
-
hustbaer schrieb:
Dabei wirst du halt wieder das Problem haben Delegates serialisieren zu müssen.
Nein, mit LINQ kann ich einen Expression-Tree statt eines kompilierten Delegates bekommen, so daß ich nicht groß mit Reflection und IL-Code arbeiten muß. Das der Grund dafür, daß LINQ to SQL funktionieren kann, und ebenso das, was meine Idee mit LINQ überhaupt praktikabel macht.
hustbaer schrieb:
als wenn das "auf Prozesse verteil-Ding" das hinter deinem LINQ Call läuft nur bestimmte Dinge unterstützt, weil das Serialisieren von Delegates nur beschränkt funktioniert.
Es sollte aber doch für alles funktionieren, was man so in ein LINQ-Query reinschreibt. Klar habe ich Schwierigkeiten, wenn der Query Seiteneffekte hat. Aber bei LINQ ist das irgendwie Teil der guten Erziehung. Bei einer imitierten for-Schleife nicht.
-
Hm, OK.
Doof wirds halt wenn der Expression-Tree ne InvocationExpression enthält. Da haste dann erst wieder einen fertig kompilierten Delegate.
BTW: Du weisst schon dass du keinen Query-Provider implementieren musst nur um an die LINQ-Expression zu kommen.
Dank dunkler Compiler-Magick funktioniert das auch so:static ICollection<TOutput> Map<TInput, TOutput>(IEnumerable<TInput> collection, System.Linq.Expressions.Expression<Func<TInput, TOutput>> expression) { var result = new List<TOutput>(); var func = expression.Compile(); foreach (TInput element in collection) result.Add(func(element)); return result; } void Foo() { var strings = new List<string>() { "foo ", " bar" }; var result = Map(strings, s => s.Trim()); }
Wäre vielleicht zum Austesten der Interprocess-Geschichte einfacher als
IQueryProvider
zu implementieren.