Laden...

[ExtensionMethods] CrossThread-Calls

Erstellt von ErfinderDesRades vor 15 Jahren Letzter Beitrag vor 15 Jahren 12.093 Views
ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren
[ExtensionMethods] CrossThread-Calls

WinForm-Gui verträgt sich schlecht mit Threading - aus einem Nebenthread darf keinesfalls auf ein Steuerelement zugegriffen werden.
Andererseits führen zeitaufwändige Arbeiten zum "Einfrieren" der Anwendung, weil während der Arbeit natürlich kein Steuerelement mehr reagieren kann.
Die Lösung:*Der aufwändige Job muß isoliert und innerhalb eines Nebenthreads abgearbeitet werden *Nach Erledigung muß die Weiter-Verarbeitung (mindestens die "Erfolgsmeldung") wieder im Gui-Thread stattfinden, denn ohne Steuerelemente kann nichts gemeldet werden.

Folglich braucht man einen "Verlege-Mechanismus", der einen Methoden-Aufruf in den Nebenthread verlegt, und einen, der einen Methoden-Aufruf in den Gui-Thread verlegt.
Diese Mechanismen existieren, sind aber in der Anwendung recht umständlich:*Gui-Thread -> NebenThread:
System.IAsyncResult ConcreteDelegate.BeginInvoke({verschiedene Argumente}, System.AsyncCallback callback, object object) *NebenThread -> Gui-Thread:
System.IAsyncResult Control.BeginInvoke(System.Delegate method, params object[] args)

Auf die Tücken von IAsyncResult und AsyncCallback gehe ich hier nicht ein, das möge man hier nachlesen. b) hat außerdem die Komplikation, daß zusätzlich zum Delegaten ein konkretes Control erforderlich ist (egal welches!). Aber was nun, wenn etwa eine Datenklasse ein Ereignis feuern will? - da ist kein Control verfügbar.

Also habich ein paar statische Methoden in einer statischen Klasse geproggt, die die Umständlichkeiten wegkapseln. Die zu verlegenden Methoden brauchen nicht geändert zu werden, sie werden nur modifiziert aufgerufen (Beispiel anhand einer Methode void Calculate(DateTime, Point) ) :


Calculate(DateTime.Now, e.Location);                           // Standard-Aufruf im akt. Thread
Crossthread.RunAsync(Calculate, DateTime.Now, e.Location);     // verlagert in NebenThread
Crossthread.RunGui(Calculate, DateTime.Now, e.Location);       // zurück-verlagert in Gui-Thread

Die Tatsache, daß die Crossthread-Methoden auch Extension-Methods generischer Action-Delegaten sind, wird häufig gar nicht ausgenutzt, weil das eine Delegat-Variable erfordert, die extendet werden kann.
Hat man eine solche, so stehen die 3 Aufruf-Varianten allerdings mit ganz identischer Signatur zur Verfügung, was den Umgang sehr erleichtert:


// ( Action<DateTime, Point> calculate = Calculate; )
calculate(DateTime.Now, e.Location);              // Standard-Aufruf im akt. Thread
calculate.RunAsync(DateTime.Now, e.Location);     // verlagert in NebenThread
calculate.RunGui(DateTime.Now, e.Location);       // zurück-verlagert in Gui-Thread

Ein wesentlicher Trick der CrossThreadX-Klasse besteht darin, daß als invokendes Control einfach Application.OpenForms[0] hergenommen wird. Das ist global zugreifbar, solange überhaupt irgendein Form angezeigt wird (und andernfalls ist eine WinForm-Anwendung i.A. ja beendet). Damit können also auch Klassen, die sonst nichts vom Gui wissen, Methoden sicher im Gui-Thread ausführen, insbesondere auch Events auslösen.

Edit: Den Original-Code habich jetzt rausgeschmissen, zugunsten des Uploades.
Der Upload enthält eine Version mit Extension-Methods (Framework 3), und eine für Framework 2, und je eine Sample-Solution, die die Verwendung der CrossThread-Klasse mit der (herkömmlichen) Verwendung eines Backgroundworkers vergleicht.
Jetzt auch eine im Nebenthread arbeitende Klasse, die ein Event im Gui-Thread auslöst.

Schlagwörter: <Threading, crossthread, Invoke, BeginInvoke,Gui,blockieren,einfrieren>

Der frühe Apfel fängt den Wurm.

1.200 Beiträge seit 2007
vor 15 Jahren
using System;
using System.Runtime;
using System.Threading;
using System.Windows.Forms;

public static class CrossThreadX
{

    private static WaitCallback callback = new WaitCallback(Invocation);

    class TargetInfo
		{
			internal TargetInfo(Delegate d, object[] args)
			{
				Target = d;
				Args = args;
			}

			internal readonly Delegate Target;
			internal readonly object[] Args;
		}



    public static void RunAsync(this Delegate d, params object[] args)
    {
        ThreadPool.QueueUserWorkItem(callback, new TargetInfo(d, args));
    }



    private static void Invocation(object o)
    {
        TargetInfo ti = (TargetInfo)o;
        ti.Target.DynamicInvoke(ti.Args);
    }


    private static bool InvokeGui(Delegate d, params object[] Args)
    {
        if (Application.OpenForms.Count == 0)
        {
            //wenn kein Form mehr da ist, so tun, als ob das Invoking ausgeführt wäre
            return true;
        }
        if (Application.OpenForms[0].InvokeRequired)
        {
            Application.OpenForms[0].BeginInvoke(d, Args);
            return true;
        }
        return false;
    }

    public static void RunGui(this Delegate d, params object[] args)
    {
        if (!InvokeGui(d, args)) Invocation(new TargetInfo(d, args));
    }

}

Hab mir mal die Freiheit genommen und den Code ein wenig in Richtung General Purpose verschoben. Was mich noch stört ist das Application.OpenForms[0], könnte ja sein, dass auf ganz einer anderen Form operiert wird.

Shift to the left, shift to the right!
Pop up, push down, byte, byte, byte!

YARRRRRR!

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

Hmm, damit hast du aber die Typisierung in die Tonne getreten.

    public static void RunAsync(this Delegate d, params object[] args)

Da kann ja jedem Delegaten alles untergeschoben werden.

Edit
Zu Application.Openforms[0]:
Ich hab das mal fett und umständlich umgebaut, undn System.Threading.SynchronisationContext eingebaut, weil jemand meinte, das sei sauberer, und dann habich im Reflector geguckt - letztlich macht der SynchronisationContext beiner WinForm-Application auch nix anneres als Control.BeginInvoke().
Und Control.BeginInvoke macht

        return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);

und ich glaub fast, FindMarshalingControl() gibt das Openforms[0] zurück, sicher binnich natürlich nicht.

Der frühe Apfel fängt den Wurm.

1.200 Beiträge seit 2007
vor 15 Jahren

Ja klar, Kindersicherung fehlt da.

Shift to the left, shift to the right!
Pop up, push down, byte, byte, byte!

YARRRRRR!

F
69 Beiträge seit 2006
vor 15 Jahren

Hi ErfinderDesRades,

es ist toll, dass du CrossThread(X) gemacht hast. Ich habe hier ein Problem:

Unter .NET Framework konnte ich CrossThreadX ohne Probleme einsetzen. Aber ich sollte das Programm möglichst .NET Framework 2 kompitabel machen und habe probiert, CrossThreadX.cs gegen CrossThread.cs auszutauschen.

Hier ist Fehlermeldung:

Fehler 1 Die Verwendung von Typ "System.Action<T>" (generisch) macht das 1-Typargument erforderlich. (Zeile: 10, 15, 22, 38, 42 und 48)

Habei ich da irgendwas vergessen?

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

Obiger Zip enthält jetzt eine 05er und eine 08er Version 🙂

Der frühe Apfel fängt den Wurm.

F
69 Beiträge seit 2006
vor 15 Jahren

Ich weiß, ich habe 05er version nur das "CrossThread.cs" datei rausgeholt und in mein Projekt implementiert. Dann kam fehlermeldung raus, nachdem ich mein VS auf Zielframe 2.0 umgestellt habe.

365 Beiträge seit 2004
vor 15 Jahren

Hallo,

irgendwie stehe ich hier gerade auf dem Schlauch:


  if (Application.OpenForms.Count == 0)
        {
            //wenn kein Form mehr da ist, so tun, als ob das Invoking ausgeführt wäre
            return true;
        }

Sollte er nicht lieber false zurückgeben damit das Action Delegate trotzdem ausgeführt wird. Falls die CrossThread Geschichte allgemein eingesetzt wird aber in dem Fall gar nicht von einer Windows.Forms Anwendung, sondern von irgendeiner Library verwendet wird, würde das Action Delegate doch jetzt gar nicht ausgeführt werden oder sehe ich das falsch.

Eigentlich müsste es dann an dieser Stelle doch so behandelt werden als wenn kein Invoke erforderlich ist, oder nicht? am Kopf kratz

Gruß

Christoph

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

Hintergrund ist, es ist nur für WinForms gebastelt.
Und wenn da kein Form mehr da ist, dann ist die Anwendung beendet.
Sowas tritt auf, wenn ein Job im Nebenthread gestartet wurde, und in der ZwiZeit die Anwendung zugemacht wurde. Dann will der Nebenthread noch seine Erfolgsmeldung absetzen, aber das Gui ist nicht mehr da.
Und da tu ich halt so, als wärs schon passiert, damit nicht auf Steuerelemente zugegriffen wird, die schon disposed sind.
Einfach, dass die Anwendung schließen kann ohne Exception.

Der frühe Apfel fängt den Wurm.

365 Beiträge seit 2004
vor 15 Jahren

Ich würde das ganze halt gerne für eine JobQueue benutzen. Die Queue arbeitet die Jobs alle hintereinander ab und erstellt für jeden Job einen neuen Thread. Wenn der Job abgearbeitet ist, wird ein Event gefeuert. Dieses Event soll jetzt allerdings wieder in dem Haupthread (den der Queue) gefeuert werden.

Auf diese Weise braucht der Entwickler, der die Queue verwendet im GUI nicht invoken. Das klappt auch alles super. Nur möchte ich natürlich auch sicherstellen, dass diese Queue auch in einer Nicht-WindowsForms Umgebung funktioniert.

Deswegen dachte ich, dass es theoretisch reichen sollte, wenn ich an der besagten Stelle false zurückgebe, was dann wiederum bedeutet, dass das Action Delegate direkt ausgeführt werden kann.

Allerdings stört mich der Verweis auf System.Windows.Forms generell. Es sollte halt einfach so sein, dass das Event im Thread der Queue und nicht im Thread des Jobs gefeuert wird und ob der Thread der Queue dann zufällig der GUI Thread ist oder eben einfach nur der Hauptthread sollte eigentlich keine Rolle spielen. Vielleicht sollte ich mir hierfür die SynchronisationContext Geschichte noch mal ansehen...

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo bvsn,

an dieser Stelle sollten wir nur festhalten, dass das Snippet eben für Windows-Forms ausgelegt ist und deshalb auch konsequenterweise true zurückgegeben wird. Wenn du zu deinem Fall noch Fragen hast, mach bitte einen neuen Thread auf. [EDIT]Siehe CrossThreadX - WorkerJobQueue[/EDIT]

herbivore