2009-05-12 11 views
0

Ich benutze BackgroundWorker die meiste Zeit in den Win-Form-Apps, um den Fortschritt zu zeigen, wie ich Daten bekomme. Ich war unter der Annahme, dass Work_completed garantiert auf dem Haupt-UI-Thread ausgeführt wird, aber es ist nicht. Wenn wir einen Thread erstellen und den worker.RunWorkerAsync darin aufrufen, bricht es ab, wenn wir versuchen, ein GUI-Steuerelement zu aktualisieren. Hier ein Beispiel:BackgroundWorkerThread Zugriff in einem Thread

private void StartButton_Click(object sender, EventArgs e) 
{ 
    Thread thread1 = new Thread(new ThreadStart(PerformWorkerTask)); 
    _worker = new BackgroundWorker(); 
    thread1.Start(); 
} 

public void PerformWorkerTask() 
{ 
    _worker.DoWork += delegate 
    { 
     for (int i = 0; i < 10; i++) 
     { 
      Thread.Sleep(100); 
     } 
    }; 

    _worker.RunWorkerCompleted += delegate 
    { 
     // this throws exception 
     MessageLabel.Text = "Completed"; 
    }; 
    _worker.RunWorkerAsync(); 

} 

Wie können wir in diesem Fall Hintergrundarbeiter arbeiten lassen?

+0

es hier scheinen slike thread1 ist ohne Grund geschaffen, aber es verwendet wird, nur einen Fall dar, in dem Hintergrundarbeiter wird von einem Nicht-GUI-Thread aufgerufen. – Sheraz

+0

Gibt es keinen generischen Ansatz, den wir annehmen können? Wie beim Schreiben einer generischen Komponente, die es uns erlaubt, die GUI unabhängig von einem Thread zu aktualisieren, ist sie bereits ausgeführt. – Sheraz

+0

Leider nein. Wenn Sie direkt auf die Steuerelemente zugreifen, müssen Sie sie nur über ihren UI-Thread ändern. Sie können in Betracht ziehen, den Nicht-UI-Thread als BackgroundWorker auszuführen und Ereignisse durch die UI zu leiten, um eine Synchronisation ohne explizites Aufrufen von Control.Invoke zu erreichen, aber am Ende geht alles durch Control.Invoke, wenn es von einem anderen Thread kommt. –

Antwort

1

RunWorkerAsync hat seine Thread-Synchronisation Magie durch die SynchronizationContext aus dem Thread bekommen, dass es aufgerufen wird. Es garantiert dann, dass die Ereignisse entsprechend der Semantik des SynchronizationContext auf dem richtigen Thread ausgeführt werden. Im Fall der WindowsFormsSynchronizationContext, die bei Verwendung von WinForms automatisch verwendet wird, werden die Ereignisse synchronisiert, indem sie in der Nachrichtenwarteschlange des Threads, der den Vorgang gestartet hat, veröffentlicht werden. Natürlich ist das alles für dich transparent, bis es bricht.

EDIT: Sie müssen RunWorkerAsync aus dem UI-Thread aufrufen, damit dies funktioniert. Wenn Sie es nicht auf andere Weise tun können, ist Ihre beste Wette ist, den Beginn der Operation an einer Steuerung aufzurufen, so dass die Arbeiter auf dem UI-Thread gestartet wird:

private void RunWorker() 
{ 
    _worker = new BackgroundWorker(); 

    _worker.DoWork += delegate 
    { 
     // do work 
    }; 

    _worker.RunWorkerCompleted += delegate 
    { 
     MessageLabel.Text = "Completed"; 
    }; 

    _worker.RunWorkerAsync(); 
} 

// ... some code that's executing on a non-UI thread ... 
{ 
    MessageLabel.Invoke(new Action(RunWorker)); 
} 
0

In dem Code, den Sie hier vorgestellt haben, fügen Sie die Delegierten für die BackgroundWorker-Ereignisse in einem separaten Thread aus dem UI-Thread hinzu.

Versuchen Sie, die Ereignishandler im Hauptthread der Benutzeroberfläche hinzuzufügen, und Sie sollten in Ordnung sein.

0

Ich glaube, dass BackgroundWorker automatisch einen neuen Thread verwendet. Daher ist das Erstellen eines neuen Threads, nur um RunWorkerAsync aufzurufen, redundant. Sie erstellen einen Thread, nur um einen weiteren Thread zu erstellen. Was wahrscheinlich passiert, ist dies:

  1. Sie erstellen einen neuen Thread aus Thread 1 (der GUI-Thread); Rufen Sie diesen Thread 2 auf.
  2. Von Thread 2 starten Sie RunWorkerAsync, die selbst einen weiteren Thread erstellt; Rufen Sie diesen Thread 3.
  3. Der Code für RunWorkerCompleted läuft auf Thread 2, der Thread ist, der RunWorkerAsync aufgerufen.
  4. Da Thread 2 nicht mit dem GUI-Thread (Thread 1) identisch ist, erhalten Sie eine ungültige Cross-Thread-Aufrufausnahme.

(Der folgende Vorschlag verwendet VB anstelle von C#, da das ist, was ich bin mehr vertraut mit;. Ich schätze, dass Sie herausfinden, wie der entsprechenden C# Code zu schreiben, das Gleiche zu tun)

Den überflüssigen neuen Thread loswerden; Deklarieren Sie einfach _worker WithEvents, fügen Sie Handler zu _worker.DoWork und _worker.RunWorkerCompleted hinzu, und rufen Sie dann _worker.RunWorkerAsync auf, anstatt eine benutzerdefinierte PerformWorkerTask-Funktion zu definieren.

EDIT: GUI-Steuerelemente in einem Thread-sichere Art und Weise zu aktualisieren, verwenden Sie Code wie folgt aus (mehr oder weniger kopiert von this article from MSDN):

delegate void SetTextCallback(System.Windows.Forms.Control c, string t); 

private void SafeSetText(System.Windows.Forms.Control c, string t) 
{ 
    if (c.InvokeRequired) 
    { 
    SetTextCallback d = new SetTextCallback(SafeSetText); 
    d.Invoke(d, new object[] { c, t }); 
    } 

    else 
    { 
    c.Text = t; 
    } 
} 
1

Von Ihrem Beispiel ist es schwer zu sehen, was gut die Thread (thread1) ist, aber wenn Sie wirklich dieses thread1 brauchen, dann denke ich, dass Ihre einzige Option ist, MainForm.Invoke() zu verwenden, um RunWorkerAsync() (oder eine kleine Methode drum herum) auf dem Hauptthread auszuführen.

Hinzugefügt: Sie so etwas wie diese verwenden:

Action a = new Action(_worker.RunWorkerAsync); 
this.Invoke(a); 
+0

hast du es genau richtig verstanden. Ich habe thread1 verwendet, um den Fall als nicht-ui-Thread darzustellen. Kannst du ein Beispiel deines Vorschlags posten – Sheraz

+0

Ich suchte nach einem generischen Ansatz wie einer Komponente, die ich hatte, die Intelligenz hat, das Steuerelement unabhängig vom Thread zu aktualisieren, in dem es ausgeführt wird. – Sheraz

0

könnten Sie wahrscheinlich Ihre vorhandenen Code machen indem Arbeit:

this.Dispatcher.BeginInvoke(() => MessageLabel.Text = "Completed") 

statt

MessageLabel.Text = "Completed" 

Du bist wahrscheinlich verkanten Datenzugriffsprobleme haben, so müssen Sie sicherstellen, dass Sie Eigenschaften von MessageLabel auf Ihrem UI-Thread zuzugreifen. Dies ist eine Möglichkeit, dies zu tun. Einige der anderen Vorschläge sind ebenfalls gültig. Die Frage, die Sie sich stellen sollten, lautet: Warum erstellen Sie einen Thread, der nur einen BackgroundWorker-Thread erstellt? Wenn es einen Grund gibt, dann ist das in Ordnung, aber aus dem, was Sie hier gezeigt haben, gibt es keinen Grund, warum Sie den BackgroundWorker-Thread nicht von Ihrem Event-Handler erstellen und starten könnten. In diesem Fall gäbe es kein Cross-Thread-Access-Problem wegen des RunWorkerCompleted-Ereignisses Der Handler ruft seine Delegierten im UI-Thread auf.

1

Es klingt wie das Problem ist nur, dass Sie eine Änderung an einer GUI-Komponente vornehmen möchten und Sie nicht wirklich sicher sind, wenn Sie auf dem GUI-Thread sind. Dan hat einen gültigen Methode eine GUI-Komponente Eigenschaft sicher zu setzen, aber ich finde die folgende Abkürzung Methode die einfachste:

  MessageLabel.Invoke(
       (MethodInvoker)delegate 
       { 
        MessageLabel.Text = "Hello World"; 
       }); 

Wenn es irgendwelche Probleme mit diesem Ansatz sind, möchte ich über sie wissen!

+0

nein es gibt kein Problem mit diesem Ansatz aber Das einzige ist, dass es kein generalisierter Ansatz ist. Ich bin gerade dabei, eine allgemeine Komponente zu schreiben, die den Job erledigt, ohne etwas zu wissen (einfach das Leben einfacher zu machen) – Sheraz

0

Der beste Weg, um mit diesen generischen Problemen umzugehen, ist, es einmal zu behandeln. Hier poste ich eine kleine Klasse, die den backgroupdworker-Thread umschließt und sicherstellt, dass das abgeschlossene Workout immer auf dem UI-Thread ausgeführt wird.


using System.Windows.Forms; namespace UI.Windows.Forms.Utilities.DataManagment { public class DataLoader { private BackgroundWorker _worker; private DoWorkEventHandler _workDelegate; private RunWorkerCompletedEventHandler _workCompleted; private ExceptionHandlerDelegate _exceptionHandler; public static readonly Control ControlInvoker = new Control();

 public DoWorkEventHandler WorkDelegate 
    { 
     get { return _workDelegate; } 
     set { _workDelegate = value; } 
    } 

    public RunWorkerCompletedEventHandler WorkCompleted 
    { 
     get { return _workCompleted; } 
     set { _workCompleted = value; } 
    } 

    public ExceptionHandlerDelegate ExceptionHandler 
    { 
     get { return _exceptionHandler; } 
     set { _exceptionHandler = value; } 
    } 

    public void Execute() 
    { 
     if (WorkDelegate == null) 
     { 
      throw new Exception(
       "WorkDelegage is not assinged any method to execute. Use WorkDelegate Property to assing the method to execute"); 
     } 
     if (WorkCompleted == null) 
     { 
      throw new Exception(
       "WorkCompleted is not assinged any method to execute. Use WorkCompleted Property to assing the method to execute"); 
     } 

     SetupWorkerThread(); 
     _worker.RunWorkerAsync(); 
    } 

    private void SetupWorkerThread() 
    { 
     _worker = new BackgroundWorker(); 
     _worker.WorkerSupportsCancellation = true; 
     _worker.DoWork += WorkDelegate; 
     _worker.RunWorkerCompleted += worker_RunWorkerCompleted; 

    } 

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 
    { 
     if(e.Error !=null && ExceptionHandler != null) 
     { 
      ExceptionHandler(e.Error); 
      return; 
     } 
     ControlInvoker.Invoke(WorkCompleted, this, e); 
    } 
} 

}

And here is the usage. One thing to note is that it exposes a static property ControlInvoker that needs to be set only once (you should do it at the beginning of the app load)

Let's take the same example that I posted in question and re write it

DataLoader loader = new DataLoader(); loader.ControlInvoker.Parent = this; // needed to be set only once

private void StartButton_Click(object sender, EventArgs e) {

Thread thread1 = new Thread(new ThreadStart(PerformWorkerTask)); 
_worker = new BackgroundWorker(); 
thread1.Start(); 

}

public void PerformWorkerTask() {

loader.WorkDelegate = delegate { // get any data you want for (int i = 0; i < 10; i++) { Thread.Sleep(100); } }; loader.WorkCompleted = delegate { // access any control you want MessageLabel.Text = "Completed"; }; loader.Execute();

}

Prost