Reputation: 751
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
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
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.
This in itself could be the problem, depending upon the capabilities of the computer you're using.
Upvotes: 1