Threadübergreifendes RaiseEvent

  • VB.NET

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

    Threadübergreifendes RaiseEvent

    Hallo,

    ich lasse eine bestimmte Aufgabe in einem extra Thread ausführen, da diese doch einige Zeit in Anspruch nimmt, die Form soll währenddessen jedoch nicht einfrieren. Wenn die Aufgabe erledigt ist, soll ein Done()-Event aufgerufen werden. Nunja, habe es mal so probiert:

    VB.NET-Quellcode

    1. Public Class Test
    2. Public Event Done()
    3. Public Sub Start()
    4. With New Thread(AddressOf DoWork)
    5. .IsBackground = True
    6. .Start()
    7. End With
    8. End Sub
    9. Private Sub DoWork()
    10. Thread.Sleep(5000) 'soll die Aufgaben darstellen
    11. RaiseEvent Done()
    12. End Sub
    13. End Class


    Nun die Form:

    VB.NET-Quellcode

    1. Public Class frmMain
    2. Public WithEvents t As New Test
    3. Private Sub btnStart_Click (...) Handles btnStart.Click
    4. t.Start()
    5. End Sub
    6. Public Sub Done() Handles t.Done
    7. Me.Text = "Done"
    8. End Sub
    9. End Class


    Mit diesem Versuch bekomme ich folgende Exception:


    Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement frmMain erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde.


    So, wenn ich es nun jedoch so mache:

    VB.NET-Quellcode

    1. Public Class Test
    2. Public Event Done()
    3. Public Sub Start()
    4. With New Thread(AddressOf DoWork)
    5. .IsBackground = True
    6. .Start()
    7. End With
    8. End Sub
    9. Private Sub DoWork()
    10. Thread.Sleep(5000) 'soll die Aufgaben darstellen
    11. InvokeDone()
    12. End Sub
    13. Private Delegate Sub InvokeDoneDelegate()
    14. Private Sub InvokeDone()
    15. If Application.OpenForms(0).InvokeRequired Then
    16. Application.OpenForms(0).Invoke(New InvokeDoneDelegate(AddressOf InvokeDone))
    17. Else
    18. RaiseEvent Done()
    19. End If
    20. End Sub
    21. End Class


    Dann klappt am Ende zwar alles, jedoch bin ich mir nicht sicher, ob das so die beste Lösung bzw ob das so nicht ein wenig umständlich ist. Auch hatte ich schon mal das Problem mit Application.OpenForms(0), dass wenn das Hauptfenster versteckt wurde (nicht mehr in der Taskleiste, sondern nur noch das NotifyIcon unten rechts in der Windowsleiste) ich eine Exception beim Aufruf bekam.
    Hey,

    Beispiel. Du hast folgende Klasse, die eine Aufgabe in einem extra Thread abarbeitet:

    VB.NET-Quellcode

    1. Imports System.ComponentModel
    2. Imports System.Threading
    3. Public Class WorkerClass
    4. Private _asyncOp As AsyncOperation
    5. Private _workerResult As Integer
    6. Public Event WorkDone(ByVal workerResult As Integer)
    7. Public Sub New()
    8. _asyncOp = AsyncOperationManager.CreateOperation(Nothing)
    9. End Sub
    10. Public Sub StartWork()
    11. Dim t As New Thread(AddressOf Worker)
    12. t.Start()
    13. End Sub
    14. Private Sub Worker()
    15. For i As Integer = 0 To 1000
    16. _workerResult += i
    17. Next
    18. _asyncOp.Post(New SendOrPostCallback(AddressOf OnWorkDone), CType(_workerResult, Object))
    19. End Sub
    20. Private Sub OnWorkDone(ByVal workResult As Object)
    21. RaiseEvent WorkDone(CType(workResult, Integer))
    22. End Sub
    23. End Class


    Die Form, die die Klasse instanziert:

    VB.NET-Quellcode

    1. Public Class Form1
    2. Private WithEvents _wC As New WorkerClass
    3. Private Sub _wC_WorkDone(workerResult As Integer) Handles _wC.WorkDone
    4. Me.Text = workerResult.ToString()
    5. End Sub
    6. Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    7. _wC.StartWork()
    8. End Sub
    9. End Class


    Leg hier ein Augenmerk auf die

    VB.NET-Quellcode

    1. Private _asyncOp As AsyncOperation
    Die Unendlichkeit ist weit. Vor allem gegen Ende. ?(
    Manche Menschen sind gar nicht dumm. Sie haben nur Pech beim Denken. 8o
    @SpaceyX: Ist das nicht etwas Overkill für die kleine Aufgabe?
    @Kraizy: Ist die Methode von SpaceyX nicht etwas Overkill für Deine Aufgabe?


    Vom Prinzip her stimmt der Code bereits. Nur statt Application.OpenForms(0).Invoke() solltest Du einfach Me.Invoke() verwenden. Das kommt wahrscheinlich im Moment aufs gleiche, da Me und Application.OpenForms(0) die selbe Form sind, aber ich denke, man sollte den sicheren Weg nehmen.

    Edit: Hab mich verlesen.
    Nimm wieder den ersten Code und verwende statt

    VB.NET-Quellcode

    1. Public Sub Done() Handles t.Done
    2. Me.Text = "Done"
    3. End Sub


    VB.NET-Quellcode

    1. Public Sub Done() Handles t.Done
    2. Me.Invoke(Sub() Me.Text = "Done")
    3. End Sub

    Das ist die simpelste Variante, die es gibt.


    PS: Soll die Test-Klasse selbst invoken? Ist das sinnvoll?
    Ich würde sagen, dass es nicht sinnvoll ist. Die Form sollte selbst wissen, dass das Test-Objekt nicht Thread-sicher ist und sollte selbst invoken (kommt auch auf ein schöneres Ergebnis raus).
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    Wenn schon, dann sollte das Invoken im Eventhandler stattfinden, denkst Du nicht? Dann hast Du auch mit "Me" den eindeutigen Zugriff.

    VB.NET-Quellcode

    1. Public Class frmMain
    2. Public WithEvents t As New Test
    3. Private Sub btnStart_Click (...) Handles btnStart.Click
    4. t.Start()
    5. End Sub
    6. Public Sub Done() Handles t.Done
    7. If Me.InvokeRequired Then
    8. Me.Invoke(Sub() Me.Text = "Done", New Object())
    9. Else
    10. Me.Text = "Done"
    11. End If
    12. End Sub
    13. End Class
    Die Unendlichkeit ist weit. Vor allem gegen Ende. ?(
    Manche Menschen sind gar nicht dumm. Sie haben nur Pech beim Denken. 8o
    Das hat damit zu tun, dass Form1 keine Instanz einer Klasse ist, sondern eine Klasse selbst.
    Du kannst auch nicht BauplanFürAuto.Herumfahren() aufrufen, weil BauplanFürAuto kein Auto ist, sondern nur der Bauplan.

    VB biegt das so hin, dass man mit BauplanFürAuto das einzige Auto meint, das nach diesem Bauplan gebaut wurde. Das ist aber schrecklich und sollte nie verwendet werden (es nennt sich Standardinstanz).

    Me ist eine Instanz von FormXY. Deshalb ist es kein Problem Me.Invoke() zu verwenden. Oder Analog dazu
    Dim Auto As New BauplanFürAuto
    Auto.Herumfahren()
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    Hi,

    @Kraizy:
    Dein Vorhaben wäre am besten mit dem BackgroundWorker gelöst. Der bietet das Event RunWorkerCompleted, und das kommt sogar automatisch im richtigen Thread.

    @Rest:
    Invoke() sollte IMMER verwendet werden, denn AsyncOperation.Post() funktioniert nur mit einem vorhandenen SynchronizationContext. Der ist im Framework nur für Forms implementiert, für alles andere muss er selbst geschrieben werden. Beispiel:

    VB.NET-Quellcode

    1. Class Form1
    2. Inherits Form
    3. Public Sub New()
    4. Dim b As New Button
    5. b.Text = "click me"
    6. b.Location = New Point(10, 10)
    7. b.Size = New Size(75, 30)
    8. AddHandler b.Click, AddressOf bclick
    9. Controls.Add(b)
    10. End Sub
    11. Private Sub bclick(sender As Object, e As EventArgs)
    12. wl("Click; UI thread id is " & Thread.CurrentThread.ManagedThreadId)
    13. Dim ao As AsyncOperation = AsyncOperationManager.CreateOperation(Nothing)
    14. Dim t As New Thread(AddressOf ClassMain.worker)
    15. t.Start(ao)
    16. End Sub
    17. End Class
    18. Class ClassMain
    19. Public Shared Sub Main(ByVal argv() As String)
    20. wl("main thread id is " & Thread.CurrentThread.ManagedThreadId)
    21. Application.Run(New Form1)
    22. wl("main thread will exit now...")
    23. rl()
    24. End Sub
    25. Public Shared Sub syncMethod(o As Object)
    26. wl("syncMethod thread id is " & Thread.CurrentThread.ManagedThreadId)
    27. End Sub
    28. Public Shared Sub worker(o As Object)
    29. Dim ao = DirectCast(o, AsyncOperation)
    30. wl("worker thread id is " & Thread.CurrentThread.ManagedThreadId)
    31. ao.Post(AddressOf syncMethod, Nothing)
    32. End Sub
    33. End Class

    Hier ist die Ausgabe auf der Konsole folgende:
    main thread id is 9
    Click; UI thread id is 9
    worker thread id is 10
    syncMethod thread id is 9
    Wir sehen, dass syncMethod korrekt synchronisiert ist und daher Zuweisungen OHNE Invoke vorgenommen werden können.

    Anderes Beispiel, ohne Form:

    VB.NET-Quellcode

    1. Class ClassMain
    2. Public Shared Sub Main(ByVal argv() As String)
    3. wl("main thread id is " & Thread.CurrentThread.ManagedThreadId)
    4. Dim ao As AsyncOperation = AsyncOperationManager.CreateOperation(Nothing)
    5. Dim t As New Thread(AddressOf worker)
    6. t.Start(ao)
    7. wl("main thread will exit now...")
    8. rl()
    9. End Sub
    10. Public Shared Sub syncMethod(o As Object)
    11. wl("syncMethod thread id is " & Thread.CurrentThread.ManagedThreadId)
    12. End Sub
    13. Public Shared Sub worker(o As Object)
    14. Dim ao = DirectCast(o, AsyncOperation)
    15. wl("worker thread id is " & Thread.CurrentThread.ManagedThreadId)
    16. ao.Post(AddressOf syncMethod, Nothing)
    17. End Sub
    18. End Class

    Ausgabe:
    main thread id is 10
    main thread will exit now...
    worker thread id is 11
    syncMethod thread id is 6
    Wir sehen, dass syncMethod gar nicht synchronisiert wurde! Das liegt am fehlenden SyncContext in der AsyncOperation-Instanz. Den Kontext müssen wir selbst bereitstellen, was aber eine Menge Arbeit bedeutet. Schließlich muss hier die ganze Invoke-Infrastruktur selbst implementiert werden.

    Wir lernen daraus: Immer schön vorsichtig mit dem AsyncOperationManager umgehen. Wer mehr darüber wissen will liest am besten folgende Artikelserie: codeproject.com/Articles/31971…hronizationContext-Part-I
    Gruß
    hal2000
    Ups - das war anders gemeint. Klar gibts ihn überall, aber zum Zwecke der Synchronisierung eben nicht ohne Mehrarbeit. Zitat aus deinem Link:
    Windows Forms provides the WindowsFormSynchronizationContext type which overrides Post to call Control.BeginInvoke.
    Wie mein zweites Codebeispiel oben zeigt, gibt es keine Implementierung, die eine derartige Synchronisierung abseits von Windows Forms (oder WPF) automatisch übernimmt. Es ist eine Sache, eine abstrakte (!) Ebene bereitzustellen, um irgendwas in geordnete Bahnen zu lenken. Wenn diese aber nicht mit Leben gefüllt wird, macht sie erstmal nichts, wie man sieht.

    Der Link ist übrigens Klasse - muss ich mal genauer lesen.
    Gruß
    hal2000