I have a object instance being displayed in a WinForms PropertyGrid. Some of the property getters call methods that will eventually call Monitor.Wait.
The problem is that Monitor.Wait does not seem to reliably block the main UI thread in this case, and it seems PropertyGrid will try to load other properties which will re-enter critical sections of code. I suppose that what is happening is the application message loop is resuming somehow even though the thread should be blocked by call to Monitor.Wait().
I've reproduced the behavior using this code:
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim inst As New Test1
Me.PropertyGrid1.SelectedObject = inst
End Sub
End Class
Public Class Test1
Private WithEvents pTimer As New System.Timers.Timer(1000) With {.AutoReset = False}
Dim rand As New Random()
Private pLock1 As New Object
Private pLock2 As New Object
Public Property Prop1 As Integer
Get
Static inst As Integer = 0
inst += 1
Return ReadVal("Prop1", inst)
End Get
Set(value As Integer)
End Set
End Property
Public Property Prop2 As Integer
Get
Static inst As Integer = 0
inst += 1
Return ReadVal("Prop2", inst)
End Get
Set(value As Integer)
End Set
End Property
Public Property Prop3 As Integer
Get
Static inst As Integer = 0
inst += 1
Return ReadVal("Prop3", inst)
End Get
Set(value As Integer)
End Set
End Property
Private pInRead As Boolean = False
Private Function ReadVal(ByVal propName As String, ByVal checkNumber As Integer) As Integer
Dim rtn As Integer = 0
Static inst As Integer = 0
Dim myinst As Integer
SyncLock pLock1
inst += 1
myinst = inst
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId & ": ReadVal enter pLock1 readval:{0}, propName:{1}, checkNumber{2}", myinst, propName, checkNumber)
If pInRead Then
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId & ": pInread already true readval:{0}, propName:{1}, checkNumber{2}", myinst, propName, checkNumber)
End If
pInRead = True
SyncLock pLock2
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId & ": waiting for pulse readval:{0}, propName:{1}, checkNumber{2}", myinst, propName, checkNumber)
System.Threading.Monitor.Wait(pLock2)
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId & ": pulse received readval:{0}, propName:{1}, checkNumber{2}", myinst, propName, checkNumber)
rtn = rand.Next(1, 100)
End SyncLock
pInRead = False
End SyncLock
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId & ": ReadVal exit pLock1 readval:{0}, propName:{1}, checkNumber{2}", myinst, propName, checkNumber)
Return rtn
End Function
Private Sub pTimer_Elapsed(sender As Object, e As Timers.ElapsedEventArgs) Handles pTimer.Elapsed
SyncLock pLock2
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId & ": timer got pLock2")
System.Threading.Thread.Sleep(1000)
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId & ": timer pulsing")
System.Threading.Monitor.Pulse(pLock2)
End SyncLock
pTimer.Start()
End Sub
Public Sub New()
pTimer.Start()
End Sub
End Class
To produce the behavior I just need to click Button1 then click around the PropertyGrid a few times. Here is the section of console output that shows the issue occurring:
8: ReadVal enter pLock1 readval:9, propName:Prop3, checkNumber3
8: waiting for pulse readval:9, propName:Prop3, checkNumber3
8: ReadVal enter pLock1 readval:10, propName:Prop1, checkNumber5
11: timer got pLock2
8: pInread already true readval:10, propName:Prop1, checkNumber5
So the issue, as shown in the output, is that ReadVal is entered in the main thread (thread id 8) to populate Prop3 in the PropertyGrid, Monitor.Wait is called, and instead of completely blocking the thread until the Pulse from the pTimer_Elapsed, we see that ReadVal is somehow entered AGAIN from the same thread (thread id 8) to populate Prop1 in the PropertyGrid. Since it's the same thread, Synclock won't actually guard the critical blocks from re-entry. In this example it doesn't matter, but in my actual application I am accessing shared resources in the locked sections and re-entry causes some big issues.
Why does main thread not reliably block when calling Monitor.Wait? Knowing that it doesn't, what is the best way to work around this issue? My solution so far is to always force ReadVal onto a new thread, but there must be a better way.