Discussion:
Threads - Join() und ManualResetEvent()
(zu alt für eine Antwort)
Josef Morlo
2011-02-24 12:45:55 UTC
Permalink
Hallo,
Ich habe zwei Fragen zu Threads, zu Join und ManualResetEvent im
besonderen.

Ein (Übungs-)Programm mit folgender Struktur: Mehrere Threads mit jeweils
Threadprozedur. Darin jeweils eine Schleife, StopFlag und Rückruf.
Schematisch:

Public Class Form1
Private mThread1 as Thread

Private mThreadN as Thread
Private mStopFlag as Boolean
Private mThreadCounter as Integer
Private Delegate Sub DelClosingForm()

Sub ThreadProc1()
While not mStopFlag
SyncLock(me)
'Tut was mit Zählern und Aktualisierung von Steuerelementen
End SyncLock
End While
'Rückmeldung, wenn Schleifendurchlauf beendet
Invoke(New DelClosingForm(AddressOf CloseForm))
End Sub

Analog die weiteren Thread-Prozeduren

Private Sub btnStart_Click(…)
mThread1 = New Thread(New ThreadStart(AddressOf ThreadProc1))
mThread1.Start()
mThreadCounter +=1

mThreadN = New Thread(New ThreadStart(AddressOf ThreadProcN))
mThreadN.Start()
mThreadCounter +=1

Form schließen

Private Sub btnExit_Click(…)
Me.Close()
End Sub

Private Sub Form1_FormClosing(…)
If mThreadCounter <> 0 Then
mStopFlag = True
e.Cancel = True
End If

'Alternativ zu CloseForm und Delegate:
'mStopFlag = True
'Do Until mThreadCounter = 0
'Application.DoEvents()
'Loop
End Sub

Sub CloseForm()
'Rückmeldung aller Threads abpassen
mThreadCounter -= 1
If mThreadCounter = 0 Then
Me.Close()
End If
End Sub
End Class

Das funktioniert auch soweit, Schleifendurchläufe und Threads werden
ordnungsgemäß beendet.

Erste Frage:
Ist es im konkreten Fall zwingend notwendig, In- und Dekrementierung des
Threadzählers abzusperren, also etwa so:

mThreadCounter = Max(Interlocked.Increment(mThreadCounter), _
(mThreadCounter - 1)) ?

Zweite Frage:
Wenn ich die Threadkonzepte recht verstanden habe, so müsste sich doch auch
die Möglichkeit bieten, den Haupthread warten zu lassen (Join) bzw. ein
Signal zu setzen (ManualResetEvent), bis bzw. wenn die Nebenthreads den
Schleifendurchgang abgeschlossen haben. Ich finde keinen Ansatzpunkt. Wie
müsste man das einbauen?

Danke für Ideen
Grüße

Josef Morlo
Armin Zingler
2011-02-24 15:39:17 UTC
Permalink
Post by Josef Morlo
Hallo,
Ich habe zwei Fragen zu Threads, zu Join und ManualResetEvent im
besonderen.
Ein (Übungs-)Programm mit folgender Struktur: Mehrere Threads mit jeweils
Threadprozedur. Darin jeweils eine Schleife, StopFlag und Rückruf.
[...]
Das funktioniert auch soweit, Schleifendurchläufe und Threads werden
ordnungsgemäß beendet.
Ist es im konkreten Fall zwingend notwendig, In- und Dekrementierung des
mThreadCounter = Max(Interlocked.Increment(mThreadCounter), _
(mThreadCounter - 1)) ?
Da die Inkrementierung/Dekrementierung ausschliesslich im
UI-Thread stattfindet, ist Interlocked.Increment/Decrement
nicht erforderlich.

Im allgemeinen: Wofür verwendest du die Max-Funktion?
Ich würde einfach nur schreiben:

Interlocked.Increment(mThreadCounter)

und

NewCounter = Interlocked.Decrement(mThreadCounter)
if Newcounter = 0 ...
Post by Josef Morlo
Wenn ich die Threadkonzepte recht verstanden habe, so müsste sich doch auch
die Möglichkeit bieten, den Haupthread warten zu lassen (Join) bzw. ein
Signal zu setzen (ManualResetEvent), bis bzw. wenn die Nebenthreads den
Schleifendurchgang abgeschlossen haben. Ich finde keinen Ansatzpunkt. Wie
müsste man das einbauen?
Dann wäre der UI-Thread aber gesperrt und das Interface "reagiert nicht".

Rein technisch gesehen: Du könntest pro Thread ein ManualResetEvent
erzeugen. Das müsstest du aber im Hintergrundthread selbst setzen,
denn Invoke würde zum dead-lock führen weil der UI-Thread
ja auf das Ende wartet. Das Warten ginge dann mit WaitHandle.WaitAll()
wobei du ein Array von ManualResetEvents übergeben kannst. Wenn
alle signalisiert sind, also alle Threads beendet, kehrt WaitAll
zurück.

Join bezieht sich nur auf /einen/ Thread. Wenn dieser ebenfalls
Invoke verwendet, käme es ebenso zum dead-lock.
--
Armin
Josef Morlo
2011-02-24 18:57:32 UTC
Permalink
Hallo Armin,
danke für Deine Rückmeldung.
Post by Armin Zingler
Post by Josef Morlo
Ist es im konkreten Fall zwingend notwendig, In- und Dekrementierung des
mThreadCounter = Max(Interlocked.Increment(mThreadCounter), _
(mThreadCounter - 1)) ?
Da die Inkrementierung/Dekrementierung ausschliesslich im
UI-Thread stattfindet, ist Interlocked.Increment/Decrement
nicht erforderlich.
Ok, ist einsichtig
Post by Armin Zingler
Im allgemeinen: Wofür verwendest du die Max-Funktion?
ich hatte die Frage mal vorläufig zurückgestellt, weil’s mir zunächst
einmal um die korrekte Steuerung und Beendigung der Threads ging. Wenn Du
auch keinen tieferen Sinn darin siehst, bin ich wohl einem Bug in den
Übersetzungshilfen aufgesessen. Ich hatte nämlich zu Übungszwecken älteren
Code (C#, VS 7.0, mit Abort() und Cross-Thread-Übergriffen) für VB 2008
aktualisiert (der Vollständigkeit halber:
<http://www.c-sharpcorner.com/UploadFile/harishankar2005/
SynchronizationinMulti-threading12032005035101AM/
SynchronizationinMulti-threading.aspx>) (ohne Umbruch)
Post by Armin Zingler
Interlocked.Increment(mThreadCounter)
und
NewCounter = Interlocked.Decrement(mThreadCounter)
if Newcounter = 0 ...
Ich werde das künftig auch so halten.
Post by Armin Zingler
Post by Josef Morlo
Wenn ich die Threadkonzepte recht verstanden habe, so müsste sich doch auch
die Möglichkeit bieten, den Haupthread warten zu lassen (Join) bzw. ein
Signal zu setzen (ManualResetEvent), bis bzw. wenn die Nebenthreads den
Schleifendurchgang abgeschlossen haben. Ich finde keinen Ansatzpunkt. Wie
müsste man das einbauen?
Dann wäre der UI-Thread aber gesperrt und das Interface "reagiert nicht".
Genau das hatte ich bei meinen Versuchen produziert!
Post by Armin Zingler
Rein technisch gesehen: Du könntest pro Thread ein ManualResetEvent
erzeugen. Das müsstest du aber im Hintergrundthread selbst setzen,
denn Invoke würde zum dead-lock führen weil der UI-Thread
ja auf das Ende wartet. Das Warten ginge dann mit WaitHandle.WaitAll()
wobei du ein Array von ManualResetEvents übergeben kannst. Wenn
alle signalisiert sind, also alle Threads beendet, kehrt WaitAll
zurück.
Join bezieht sich nur auf /einen/ Thread. Wenn dieser ebenfalls
Invoke verwendet, käme es ebenso zum dead-lock.
Ok, dann habe ich jetzt die Marschrichtung.:) Falls ich damit nicht zu
Rande kommen sollte, werde ich nochmal posten.

Herzlichen Dank für Deine Unterstützung!

Grüße

Josef Morlo
Josef Morlo
2011-02-26 14:42:10 UTC
Permalink
Hallo Armin,

bin erst heute nochmal dazugekommen - aber trotzdem kurze Rückmeldung, und
eine kleine Nachfrage.

Mir sind die Zusammenhänge jetzt klarer. Es lag natürlich an einem Invoke
(zur Aktualisierung von Textboxen) und der Aufruf musste ja, wie Du auch
geschrieben hast, während der Sperre zu einem Dead-Lock führen. WaitHandle
führt also in meinem Fall nicht weiter. Den Testcode habe ich unten
angehängt.
Post by Josef Morlo
Interlocked.Increment(mThreadCounter)
und
NewCounter = Interlocked.Decrement(mThreadCounter)
if Newcounter = 0 ...
warum nicht: mThreadCounter = Interlocked.Decrement(mThreadCounter)? Das
sollte doch keinen Unterschied machen?

Merci!

Josef Morlo


Anhang:
Imports System.Threading
Public Class Form1 ‘Button1, TextBox1, Textbox2
Private mAre(1) As AutoResetEvent
Private mFlag As Boolean
Delegate Sub DelSetTest(ByVal txt As TextBox, ByVal i As Integer)

Sub ThreadProc1()
Dim i As Integer
mAre(0) = New AutoResetEvent(False)
While Not mFlag

i += 1
End SyncLock
'If TextBox1.InvokeRequired Then
' Dim del As New DelSetTest(AddressOf SetText)
' TextBox1.Invoke(del, New Object() {TextBox1, i})
'Else
' TextBox1.Text = i.ToString
'End If
SyncLock (Me)
End While
mAre(0).Set()
End Sub
Sub ThreadProc2()
Dim i As Integer
mAre(1) = New AutoResetEvent(False)
While Not mFlag
SyncLock (Me)
i += 1
'If TextBox2.InvokeRequired Then
' Dim del As New DelSetTest(AddressOf SetText)
' TextBox2.Invoke(del, New Object() {TextBox2, i})
'Else
' TextBox2.Text = i.ToString
'End If
End SyncLock
End While
mAre(1).Set()
End Sub

Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
mFlag = True
WaitHandle.WaitAll(mAre)
MsgBox("0k")
End Sub

Private Sub Form1_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
Dim th1 As New Thread(New ThreadStart(AddressOf ThreadProc1))
th1.Start()
Dim th2 As New Thread(New ThreadStart(AddressOf ThreadProc2))
th2.Start()
End Sub
Sub SetText(ByVal txt As TextBox, ByVal i As Integer)
txt.Text = i.ToString
End Sub
End Class
Module Program
<MTAThreadAttribute()> Public Sub Main()
Application.Run(New Form1)
End Sub
End Module
Armin Zingler
2011-02-26 16:21:20 UTC
Permalink
Post by Josef Morlo
Post by Josef Morlo
Interlocked.Increment(mThreadCounter)
und
NewCounter = Interlocked.Decrement(mThreadCounter)
if Newcounter = 0 ...
warum nicht: mThreadCounter = Interlocked.Decrement(mThreadCounter)? Das
sollte doch keinen Unterschied machen?
Da mThreadCounter ByRef übergeben wird ist die Zuweisung nicht
erforderlich. Vielmehr noch, sie ist sogar falsch. Denn, sollte
zwischen der Dekrementierung durch die Decrement-Methode und
deiner Zuweisung des Funktionswerts zur Variablen ein anderer
Thread ebenso Interlocked.Decrement aufgerufen haben, ist der
Funktionswert ja schon wieder veraltet. Du überschreibst
durch deine Zuweisung also den neueren Wert, wie er im anderen
Thread geändert wurde. Der Sinn der Decrement-Methode, nämlich
die threadsichere Dekrementierung, wird dadurch ad absurdum
geführt.

An einem Beispiel durchgespielt:
Angenommen, mThreadCounter hat den Wert 4.

1. Thread 1: Ruft Decrement auf. => mThreadCounter ist 3.
Das ist auch der Funktionswert.
2. Thread 2: Ruft Decrement auf. => mThreadCounter ist 2.
3. Thread 1: Funktionswert wird der Variablen durch deinen eigenen
Code zugewiesen. mThreadCounter ist somit wieder 3.
--
Armin
Josef Morlo
2011-02-26 18:19:45 UTC
Permalink
[...]
Post by Armin Zingler
Post by Josef Morlo
Post by Armin Zingler
NewCounter = Interlocked.Decrement(mThreadCounter)
if Newcounter = 0 ...
warum nicht: mThreadCounter = Interlocked.Decrement(mThreadCounter)? Das
sollte doch keinen Unterschied machen?
Da mThreadCounter ByRef übergeben wird ist die Zuweisung nicht
erforderlich. Vielmehr noch, sie ist sogar falsch. Denn, sollte
zwischen der Dekrementierung durch die Decrement-Methode und
deiner Zuweisung des Funktionswerts zur Variablen ein anderer
Thread ebenso Interlocked.Decrement aufgerufen haben, ist der
Funktionswert ja schon wieder veraltet. Du überschreibst
durch deine Zuweisung also den neueren Wert, wie er im anderen
Thread geändert wurde. Der Sinn der Decrement-Methode, nämlich
die threadsichere Dekrementierung, wird dadurch ad absurdum
geführt.
Angenommen, mThreadCounter hat den Wert 4.
1. Thread 1: Ruft Decrement auf. => mThreadCounter ist 3.
Das ist auch der Funktionswert.
2. Thread 2: Ruft Decrement auf. => mThreadCounter ist 2.
3. Thread 1: Funktionswert wird der Variablen durch deinen eigenen
Code zugewiesen. mThreadCounter ist somit wieder 3.
Hab's verstanden!

Nochmal ein herzliches Dankeschön!

Josef Morlo

Loading...