5

Consider the following demo code attached to a button on a Windows Form:

private void button1_Click(object sender, System.EventArgs e)
{
    var semaphore = new SemaphoreSlim(0, 1);

    Invalidate();  // <-- posts a message that surprisingly will be processed while we're waiting
    Paint += onPaint;
    semaphore.Wait(1000);
    Paint -= onPaint;

    void onPaint(object s, PaintEventArgs pe)
    {
        throw new System.NotImplementedException();  // we WILL hit this!
    }
}

Although we are hanging on a semaphore wait on the UI thread, the paint message posted by Invalidate() will still be executed while we're hanging on the Wait() - AND (of course) on the UI thread.

This demonstrates the root cause for a bug which I'm trying to create a failing Unit Test for - without using any Windows Form. I've been playing with custom SyncronizationContexts and TaskSchedulers for a few hours now but I have not been able to make this happen on the same thread.

What I would like to do, in pseudo code, would be something like:

[Test]
public void Test()
{
    // something magic - this doesn't help:
    // SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

    SynchonizationContext.Current.Post(MethodToBeExecutedWhileWeAreWaitingAndOnThisVeryThread);
    var semaphore = new SemaporeSlim(0, 1);
    AssertThatMethodHasNotBeenCalled();
    semaphore.Wait(1000);
    AssertThatMethodGotCalled();
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Dan Byström
  • 9,067
  • 5
  • 38
  • 68
  • 3
    This [question and answer](https://stackoverflow.com/questions/21211998/stataskscheduler-and-sta-thread-message-pumping) alludes to the fact that the `Wait` for `SemaphoreSlim` does use a primitive that also pumps window messages, so yes, it happens. – Damien_The_Unbeliever Aug 10 '21 at 13:19
  • 3
    Even without that, though (I take your word for it), couldn't `onPaint` be hit here before `semaphone.Wait()` is? – 500 - Internal Server Error Aug 10 '21 at 13:21
  • 3
    Not sure the unit test idea will be practical. You'd at the least need to make sure you're running your test on an STA thread that is actually running a message pump. – Damien_The_Unbeliever Aug 10 '21 at 13:21
  • @500-InternalServerError and absolutely NO. – Dan Byström Aug 10 '21 at 13:27
  • @Damien_The_Unbeliever Well, I was hoping for a way to simulate a simple message pump. Practical or not, I need a test that will break if someone removes my bug-fix. – Dan Byström Aug 10 '21 at 13:29
  • Are you sure that `Invalidate` **posts** a message? For me it looks like `Invalidate` is calling [`InvalidateRect`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-invalidaterect) which **sends** a WM_PAINT message. So I would expect the described behavior. – Steeeve Aug 10 '21 at 13:32
  • @Steeeve I'm sure Invalidate() does not call Paint immediately, yes. Refresh() does - Invalidate() does not. I have edited the question and moved it up one line to remove any confusion. – Dan Byström Aug 10 '21 at 13:36
  • 1
    What happens if you replace the `semaphore.Wait(1000);` with `Thread.Sleep(1000);`? Does the behavior change? – Theodor Zoulias Aug 10 '21 at 14:03
  • @DanByström Invalidate() doesn't call Paint - true. But it sends the WM_PAINT message, which will be processed by the message pump that Damien already mentioned. Shortened stacktrace: SemaphoreSlim.Wait() -> System.Threading.Monitor.Wait() -> WndProc() -> Control.OnPaint(). Just make a breakpoint in your onPaint to see the wohle thing. – Steeeve Aug 10 '21 at 14:08
  • 1
    @TheodorZoulias onPaint won't be called. Thread.Sleep doesn't pump messges. – Steeeve Aug 10 '21 at 14:10
  • @Steeeve I'm not sure what you're aiming at. I have fixed a bug deep in a library that uses SemaphoreSlim.Wait and didn't expect any reentrancy, which apparently could happen, as my code demonstrates. Now I need to write a Unit Test that will fail if someone removes my fix. Simple as that. – Dan Byström Aug 10 '21 at 14:17
  • @DanByström just to be sure I understood your goal: you would like to reproduce reentrancy caused by windows forms + semaphore.wait without using windows forms? Are native windows apis allowed? – Steeeve Aug 10 '21 at 14:50
  • @Steeeve *smiling big and nodding frantically*. I would like to somehow trick SemaphoreSlim.Wait to make a call to my code so I can make the (once) fatal re-entrant call and verify that it is no longer an issue. – Dan Byström Aug 10 '21 at 14:58
  • @Steeeve `InvalidateRect()` doesn't cause WM_PAINT at all. It accumulates in the drawing Region. The Region is painted when **the next** WM_PAINT message is sent. Usually performed when `Control.Update()` is called (which in turn calls `UpdateWindow()`, which sends WM_PAINT). That's what `Control.Refresh()` does (`RedrawWindow()` + `UpdateWindow()`). – Jimi Aug 10 '21 at 14:59
  • 1
    @Jimi *The system sends a WM_PAINT message to a window whenever its update region is not empty and there are no other messages in the application queue for that window.* I tought that this means, the function InvalidateRect self will send this message. But I'm pretty sure, it doesn't make a difference here. The WM_PAINT is in the message queue and gets processed. – Steeeve Aug 10 '21 at 17:15
  • 1
    @DanByström I've tried out everything I can, nothing worked... I have no more ideas how to accomplish this without a visible window. But I've learned interesting things and am very curious if somebody get's this working:) – Steeeve Aug 10 '21 at 17:21
  • 1
    I don't think that it make sense to write such unit test as you already spend many hours on that and the test you write might not even prove anything useful. Simply write a comment in the code about the fix. If desired, you write add some extra check in the code too. – Phil1970 Aug 10 '21 at 17:26
  • @Steeeve *learned interesting things* sums it up pretty well for me too! Many thanks for your thoughts and effort! – Dan Byström Aug 10 '21 at 18:19
  • @Phil1970 yeah, with my current understading that's the best/only thing I can do. i didn't want to drop it without having asked here if there was a known way! – Dan Byström Aug 10 '21 at 18:23
  • @Steeeve Yes, the *System*, not the function itself, so a message is posted (not sent, it's not sync) and enqueued - it's obvious *where* - which is the whole point here: to be executed, the message queue must be pumping at that point. The first comment in this long list implies the rest. – Jimi Aug 10 '21 at 18:46
  • It's umm...'unfortunate' that a synchro object names a 'semaphore' does not actually block when there are no units available. It's not a semaphore and should not be names as such. In general, blocking calls should not be made in a GUI event handler, (except for test/debug, as here:). This 'SemaphoreSlim' object is an example of the messes that occur when language designers try to give inexperienced developers band-aids to cover up bad designs:( – Martin James Aug 11 '21 at 07:42
  • Does the same happens if you use a `ManualResetEventSlim` instead of a `SemaphoreSlim`? `var handle = new ManualResetEventSlim(); handle.Wait(1000);` And what about the `Monitor`? Does it behave the same? `object locker = new(); Task.Run(() => Monitor.Enter(locker)).Wait(); Monitor.TryEnter(locker, 1000);` – Theodor Zoulias Aug 11 '21 at 18:44
  • 1
    @TheodorZoulias Same thing. I just happened to find this "Thankfully, the CLR pumps for you whenever you block in managed code (via a call to a contentious Monitor.Enter, WaitHandle.WaitOne, FileStream.EndRead, Thread.Join, and so forth)". So this apparently is considered a great feature. :-/ https://learn.microsoft.com/en-us/archive/msdn-magazine/2006/april/avoiding-and-detecting-deadlocks-in-net-apps-with-csharp-and-c – Dan Byström Aug 12 '21 at 06:34
  • @DanByström good find! You discovered what a "pumping wait" is. You could consider posting a quote from this article as an [answer](https://stackoverflow.com/help/self-answer) to this question. – Theodor Zoulias Aug 12 '21 at 07:44
  • @TheodorZoulias Well, my question really is: How can I mock this "pumping" in a Unit Test? – Dan Byström Aug 12 '21 at 08:00
  • @DanByström yeap, you are right, the question is about mocking this behavior. Seems difficult to do it without showing a `Form`. It might be just impossible. – Theodor Zoulias Aug 12 '21 at 09:24

0 Answers0