tags:

views:

45

answers:

2

Hello, comrades) I've found some interesting behavior of Invalidate method in multithreaded applications. I hope you could help me with a problem...

I experience troubles while trying to invalidate different controls at one time: while they're identical, one succesfully repaints itself, but another - not.

Here is an example: I have a form (MysticForm) with two panels (SlowRenderPanel) on it. Each panel has a timer and with a period of 50ms Invalidate() method is called. In OnPaint method I draw number of current OnPaint call in the centre of panel. But notice that in OnPaint method System.Threading.Thread.Sleep(50) is called to simulate long time draw procedure.

So the problem is that the panel added first repaints itself much more often than another one.

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1 {
    static class Program {
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MysticForm());
        }
    }

    public class MysticForm : Form {
        public SlowRenderPanel panel1;
        public SlowRenderPanel panel2;

        public MysticForm() {
            // add 2 panels to the form
            Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Left, BackColor = Color.Red, Width = ClientRectangle.Width / 2 });
            Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Right, BackColor = Color.Blue, Width = ClientRectangle.Width / 2 });
        }
    }

    public class SlowRenderPanel : Panel {
        // synchronized timer
        private System.Windows.Forms.Timer timerSafe = null;
        // simple timer
        private System.Threading.Timer timerUnsafe = null;
        // OnPaint call counter
        private int counter = 0;

        // allows to use one of the above timers
        bool useUnsafeTimer = true;

        protected override void Dispose(bool disposing) {
            // active timer disposal
            (useUnsafeTimer ? timerUnsafe as IDisposable : timerSafe as IDisposable).Dispose();
            base.Dispose(disposing);
        }

        public SlowRenderPanel() {
            // anti-blink
            DoubleBuffered = true;
            // large font
            Font = new Font(Font.FontFamily, 36);

            if (useUnsafeTimer) {
                // simple timer. starts in a second. calls Invalidate() with period = 50ms
                timerUnsafe = new System.Threading.Timer(state => { Invalidate(); }, null, 1000, 50);
            } else {
                // safe timer. calls Invalidate() with period = 50ms
                timerSafe = new System.Windows.Forms.Timer() { Interval = 50, Enabled = true };
                timerSafe.Tick += (sender, e) => { Invalidate(); };
            }
        }

        protected override void OnPaint(PaintEventArgs e) {
            string text = counter++.ToString();

            // simulate large bitmap drawing
            System.Threading.Thread.Sleep(50);

            SizeF size = e.Graphics.MeasureString(text, Font);
            e.Graphics.DrawString(text, Font, Brushes.Black, new PointF(Width / 2f - size.Width / 2f, Height / 2f - size.Height / 2f));
            base.OnPaint(e);
        }

    }

}

Debug info:

1) Each panel has a bool field useUnsafeTime (set to true by default) which allows using System.Windows.Forms.Timer (false) insted of System.Threading.Timer (true). In the first case (System.Windows.Forms.Timer) everything works fine. Removing System.Threading.Sleep call in OnPaint also makes execution fine.

2) Setting timer interval to 25ms or less prevents second panel repainting at all (while user doesn't resize the form).

3) Using System.Windows.Forms.Timer leads to speed increasement

4) Forcing control to enter synchronization context (Invoke) doesn't make sense. I mean that Invalidate(invalidateChildren = false) is "thread-safe" and could possibly have different behavior in diffenent contexts

5) Nothing interesting found in IL comparison of these two timers... They just use different WinAPI functions to set and remove timers (AddTimerNative, DeleteTimerNative for Threading.Timer; SetTimer, KillTimer for Windows.Forms.Timer), and Windows.Forms.Timer uses NativeWindow's WndProc method for rising Tick event

I use a similar code snippet in my application and unfortunately there is no way of using System.Windows.Forms.Timer) I use long-time multithreaded image rendering of two panels and Invalidate method is called after rendering is completed on each panel...

That would be great if someone could help me to understand what's different happening behind the scenes and how to solve the problem.

P.S. Interesting behavior isn't it?=)

+1  A: 

Invalidate() invalidates the client area or rectangle ( InvalidateRect() ) and "tells" Windows that next time Windows paints; refresh me, paint me. But it does not cause or invoke a paint message. To force a paint event, you must force windows to paint after an Invalidate call. This is not always needed, but sometimes it's what has to be done.

To force a paint you have to use the Update() function. "Causes the control to redraw the invalidated regions within its client area."

You have to use both in this case.


Edit: A common technique to avoid these kinds of problems is keep all your paint routines and anything related in a single (generally main) thread or timer. The logic can run elsewhere but where the actual paint calls are made should all be in one thread or timer.

This is done in games and 3D simulations.

HTH

JustBoo
JustBoo: it causes even more strange things happen: form doesnt's response, first panel repaints ~10-20 times then another and so on. I think the main thing for us is to understand real difference between two timers, cause in case of System.Windows.Forms.Timer system works perfect... with the same "Invalidate()" code...
Mikant
See new edit. (More text to satisfy filter.)
JustBoo
but what could be easier than paint two bitmaps on two panels with some frequency... i already have the bitmaps and issue low performance only in Graphics.DrawImageUnscaled method... so how can i change the code just to call repaint for both of then at a moment? =(
Mikant
IMO, have one timer at the form-level that causes your own "Animation Event" based on an interval you want. Draw everything in this one event/call. If the interval is fast enough, 18 to 30 times a second, it should look okay. This also has the benefit that all your drawing calls are now in one place for ease of maintenance.
JustBoo
see comments for Hans Passant answer. i've already tried this... same thing
Mikant
Hey, wait a minute. I may have misunderstod something. I though you were tyring to animate something with timers. If you want to display photos just do what we talked about in form_load(). :-) Or am I still missing something.
JustBoo
)) photos with changing width are just an example. I render thousands of data records, draw a bitmap in memory, and after rendering is completed i call invalidate method to see the changes. render thread just copies the bitmap to main thread storage...
Mikant
when the control was standalone everything went fine for me, but now i have two similar panels. my example in the question perfectly describes my problem. just copy the code in VS
Mikant
A: 

Nice demonstration of what goes wrong when you use members of a control or form on a background thread. Winforms usually catches this but there's a bug in the Invalidate() method code. Change it like this:

 timerUnsafe = new System.Threading.Timer(state => { Invalidate(true); }, null, 1000, 50);

to trip the exception.

The other panel is slower because lots of its Invalidate() calls are getting canceled by the paint event. Which is just slow enough to do so. Classic threading race. You cannot call Invalidate() from a worker thread, the synchronous timer is an obvious solution.

Hans Passant
I simplified the "task"... just disable panel timers and add this line of code into MysticForm ctor new System.Threading.Timer(state => { Invoke(new MethodInvoker(() => { Invalidate(true); })); }, null, 1000, 50);we should not see THE PROBLEM but it still exists. there shouldn't be a race condition.am I right?=)
Mikant
Erm, you're invoking faster than the UI thread can keep up. You'll eventually die on OOM. And there's another threading race when you close the form. Is there any point in trying to make this work? What are you *really* trying to do.
Hans Passant
(if i correctly understood you)i have two large bitmaps (1000x500) and want to paint them on two panels at a moment (for example - 2 photos, width-binded), but the time of Invalidate call is being defined in another thread... (for example - every 50ms it gives photos (new) random width)
Mikant
Well, it's pointless. And it is dangerous although you'll probably won't fall in the deadlock trap in this case. Trying to get the UI thread to update the images faster than it can possibly paint them just won't work. You've got a human eye looking at this, a synchronous timer will work just fine. Get faster image updates by storing the images in the 32bppPArgb format, it paints about 10 times faster than any of the others. And pre-size the images, rescaling them in the Paint event is expensive.
Hans Passant
Although pre-sizing them probably won't be useful if you keep changing their size.
Hans Passant
thx 4 patience, i've realized my mistake: System.Windows.Forms.Timer doesn't fire Tick event while control is "busy". private static volatile bool isPainting; solved my problem
Mikant
Yes, that's what synchronous timer means. A volatile isPainting flag won't be nearly good enough, you cannot implement a thread-safe flag with the volatile keyword. A Mutex is required, if you actually still use the System.Thread.Timer
Hans Passant