0

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.

Odoth
  • 526
  • 4
  • 6
  • 3
    Why do you want to block the UI thread? Most times we try to code to avoid doing that. – Enigmativity Jan 24 '16 at 03:11
  • It just happens to be the UI thread because the behavior is caused by PropertyGrid being populated. The issue is that after calling Monitor.Wait, somehow there is still activity on the main thread that is ultimately causing re-entry into critical code sections. I edited in more details above. – Odoth Jan 24 '16 at 05:10
  • Are you using `DoEvents()` anywhere? – Enigmativity Jan 24 '16 at 05:16
  • 2
    Monitors don't completely block the UI thread. They will keep pumping som emessags: http://stackoverflow.com/questions/8431221/why-did-entering-a-lock-on-a-ui-thread-trigger-an-onpaint-event, http://stackoverflow.com/questions/21571598/which-blocking-operations-cause-an-sta-thread-to-pump-com-messages, http://blogs.msdn.com/b/cbrumme/archive/2003/04/17/51361.aspx – shf301 Jan 24 '16 at 05:30
  • @Odoth - Then how are you getting re-entrancy? – Enigmativity Jan 24 '16 at 07:08
  • 1
    PropertyGrid.OnPaint() is enough to trigger the re-entrancy. The only reasonable fix for this is to not display the PropertyGrid until the SelectedObject is "ready". – Hans Passant Jan 24 '16 at 09:04

0 Answers0