Bill Brooks
Bill Brooks

Reputation: 751

Why do Graphics operations in background thread block Graphics operations in main UI thread?

I've got a background thread that is creating grayscale thumbnails of images in a given folder. The problem I'm seeing is that the Graphics.DrawImage() call in the background thread seems to be somehow blocking Graphics operations on the main UI thread.

I may be misinterpreting what I'm seeing here, and won't have a chance to do any in-depth profiling until later tonight, though I don't expect to be able to find much.

I've tried to come up with as small a repro case as possible. If you replace the form in a default project with the form below (and have some images in a folder to test with), you'll notice that the animating label will stutter as it bounces back and forth across the window. Yet if you uncomment the #define at the top so that a child control animates rather than redrawing the window contents, it runs perfectly smoothly.

Can anyone see what I'm doing wrong here or help me figure out how to avoid this stutter during the update loop?

//#define USE_LABEL_CONTROL

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using Timer = System.Windows.Forms.Timer;

namespace ThreadTest
{
    public partial class Form1 : Form
    {
        private const string ImageFolder = "c:\\pics";
        private const string ImageType = "*.jpg";

        public Form1()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            this.Size = new Size(300, 300);

            string[] ImageFiles = Directory.GetFiles(ImageFolder, 
                                                        ImageType, 
                                                        SearchOption.AllDirectories);

            // kick off a thread to create grayscale thumbnails of all images
            this.thumbnailThread = new Thread(this.thumbnailThreadFunc);
            this.thumbnailThread.Priority = ThreadPriority.Lowest;
            this.thumbnailThread.Start(ImageFiles);

            // set a timer to start us off...
            this.startTimer = new Timer();
            this.startTimer.Interval = 500;
            this.startTimer.Tick += this.startTimer_Tick;
            this.startTimer.Start();

#if USE_LABEL_CONTROL
            this.label.Location = this.labelRect.Location;
            this.label.Size = this.labelRect.Size;
            this.label.Text = "Loaded: 0";
            this.label.BorderStyle = BorderStyle.FixedSingle;
            this.Controls.Add(this.label);
#endif

            base.OnLoad(e);
        }

        void startTimer_Tick(object sender, EventArgs e)
        {
            // kill the timer
            this.startTimer.Stop();

            // update ourself in a loop
            while (this.IsHandleCreated)
            {
                int NextTick = Environment.TickCount + 50;

                // update the label position
                this.labelRect.Offset(this.currentLabelDirection, 0);
                if (this.labelRect.Right == this.ClientRectangle.Right ||
                    this.labelRect.Left == 0)
                {
                    this.currentLabelDirection = -this.currentLabelDirection;
                }

                // update the display
#if USE_LABEL_CONTROL
                this.label.Text = "Loaded: " + this.thumbs.Count;
                this.label.Location = this.labelRect.Location;
#else
                using (Graphics Dest = this.CreateGraphics())
                {
                    this.redrawControl(Dest, this.ClientRectangle);
                }
#endif

                Application.DoEvents();
                Thread.Sleep(Math.Max(0, NextTick - Environment.TickCount));
            }
        }

        private void thumbnailThreadFunc(object ThreadData)
        {
            string[] ImageFiles = (string[]) ThreadData;
            foreach (string ImageFile in ImageFiles)
            {
                if (!this.IsHandleCreated)
                {
                    return;
                }

                using (Image SrcImg = Image.FromFile(ImageFile))
                {
                    Rectangle SrcRect = new Rectangle(Point.Empty, SrcImg.Size);

                    Rectangle DstRect = new Rectangle(Point.Empty, new Size(300, 200));
                    Bitmap DstImg = new Bitmap(DstRect.Width, DstRect.Height);
                    using (Graphics Dst = Graphics.FromImage(DstImg))
                    {
                        using (ImageAttributes Attrib = new ImageAttributes())
                        {
                            Attrib.SetColorMatrix(this.grayScaleMatrix);
                            Dst.DrawImage(SrcImg, 
                                            DstRect, 
                                            0, 0, SrcRect.Width, SrcRect.Height, 
                                            GraphicsUnit.Pixel, 
                                            Attrib);
                        }
                    }

                    lock (this.thumbs)
                    {
                        this.thumbs.Add(DstImg);
                    }
                }
            }
        }

#if !USE_LABEL_CONTROL
        private void redrawControl (Graphics Dest, Rectangle UpdateRect)
        {
            Bitmap OffscreenImg = new Bitmap(this.ClientRectangle.Width, 
                                                this.ClientRectangle.Height);
            using (Graphics Offscreen = Graphics.FromImage(OffscreenImg))
            {
                Offscreen.FillRectangle(Brushes.White, this.ClientRectangle);
                Offscreen.DrawRectangle(Pens.Black, this.labelRect);
                Offscreen.DrawString("Loaded: " + this.thumbs.Count,
                                        SystemFonts.MenuFont,
                                        Brushes.Black,
                                        this.labelRect);
            }
            Dest.DrawImageUnscaled(OffscreenImg, 0, 0);
            OffscreenImg.Dispose();
        }

        protected override void OnPaintBackground(PaintEventArgs e)
        {
            return;
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            this.redrawControl(e.Graphics, e.ClipRectangle);
        }
#endif


        private ColorMatrix grayScaleMatrix = new ColorMatrix(new float[][] 
                                                        {
                                                            new float[] {.3f, .3f, .3f, 0, 0},
                                                            new float[] {.59f, .59f, .59f, 0, 0},
                                                            new float[] {.11f, .11f, .11f, 0, 0},
                                                            new float[] {0, 0, 0, 1, 0},
                                                            new float[] {0, 0, 0, 0, 1}
                                                        });
        private Thread thumbnailThread;
        private Timer startTimer;
        private List<Bitmap> thumbs = new List<Bitmap>();
        private Label label = new Label();
        private int currentLabelDirection = 1;
        private Rectangle labelRect = new Rectangle(0, 125, 75, 20);
    }
}

Upvotes: 3

Views: 2805

Answers (2)

Bill Brooks
Bill Brooks

Reputation: 751

It turns out that the answer is to use multiple processes to handle background GDI+ tasks. If you run the above code under the concurrency profiler in VS2010, you'll see the foreground thread blocking on a critical section secured by the DrawImage() call in the background thread.

This thread also discusses this issue and points out that since it's using a critical section, the locks will be per-process and the background tasks can be parallelized using multiple processes instead of threads:

Parallelizing GDI+ Image Resizing .net

Upvotes: 7

John Fisher
John Fisher

Reputation: 22717

Any time you have performance problems, you need to consider as many factors as possible. In your case, there's a rather large difference between the label and the redraw implementations.

  • The label version is redrawing only a small (or two small) sections of the screen.
  • The redraw version is doing much more work and redrawing a larger area.

This in itself could be the problem, depending upon the capabilities of the computer you're using.

Upvotes: 1

Related Questions