0

I have some code which takes an uploaded image, resizes it to 5 different sizes, then uploads those to another storage repository. I'm trying to execute the 5 image resizing operations in parallel using TPL.

Upfront I'll mention that the resizing function is a static method, but that it doesn't use any static resources (so multiple parallel calls shouldn't be stepping on each other, from what I can tell). Also very relevant is that this is in the context of ASP.NET which has some different threading concerns.

When I run the following code, I invariably get an "invalid parameter" error from one of the calls, though which call varies:

tasks = new Task<Uri>[]
{
    Task.Run<Uri>(() => Upload(intId, ProfilePhotoSize.FiveHundredFixedWidth, photoStream, mediaType, minWidth)),
    Task.Run<Uri>(() => Upload(intId, ProfilePhotoSize.Square220, photoStream, mediaType, minWidth)),
    Task.Run<Uri>(() => Upload(intId, ProfilePhotoSize.Square140, photoStream, mediaType, minWidth)),
    Task.Run<Uri>(() => Upload(intId, ProfilePhotoSize.Square80, photoStream, mediaType, minWidth)),
    Task.Run<Uri>(() => Upload(intId, ProfilePhotoSize.Square50, photoStream, mediaType, minWidth))
};

await Task.WhenAll(tasks)

If I look at the images created, some will be ok, some will clearly be corrupted - and this applies not just to the "errored" image.

However, executing those five operations synchronously results in five good images:

Upload(intId, ProfilePhotoSize.FiveHundredFixedWidth, photoStream, mediaType, minWidth);
Upload(intId, ProfilePhotoSize.Square220, photoStream, mediaType, minWidth);
Upload(intId, ProfilePhotoSize.Square140, photoStream, mediaType, minWidth);
Upload(intId, ProfilePhotoSize.Square80, photoStream, mediaType, minWidth);
Upload(intId, ProfilePhotoSize.Square50, photoStream, mediaType, minWidth);

Is there a known issue with these sorts of operations, or have I maybe done something else dodgy that may be causing this?

Here is the image resizing function:

private static Stream Resize(Stream image, ResizeParameters parameters, ImageUtility.ResizeType type)
{
  Image image1 = (Image) new Bitmap(image);
  Image image2 = (Image) null;
  Image image3 = (Image) null;
  Graphics graphics = (Graphics) null;
  MemoryStream memoryStream = new MemoryStream();
  try
  {
    parameters = ImageUtility.CalculateSize(image1, parameters, type);
    if (!parameters.DoNothing)
    {
      image2 = (Image) new Bitmap(parameters.Width, parameters.Height);
      switch (type)
      {
        case ImageUtility.ResizeType.FixedWidth:
          graphics = Graphics.FromImage(image2);
          graphics.SmoothingMode = SmoothingMode.AntiAlias;
          graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
          graphics.FillRectangle(Brushes.White, 0, 0, image1.Width, image1.Height);
          graphics.DrawImage(image1, 0, 0, parameters.Width, parameters.Height);
          graphics.Dispose();
          break;
        case ImageUtility.ResizeType.PaddingSquare:
          image3 = (Image) new Bitmap(parameters.SelectedWidth, parameters.SelectedHeight);
          graphics = Graphics.FromImage(image3);
          graphics.SmoothingMode = SmoothingMode.AntiAlias;
          graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
          graphics.FillRectangle(Brushes.White, 0, 0, parameters.SelectedWidth, parameters.SelectedHeight);
          graphics.DrawImage(image1, parameters.X, parameters.Y, image1.Width, image1.Height);
          graphics = Graphics.FromImage(image2);
          graphics.DrawImage(image3, 0, 0, parameters.Width, parameters.Height);
          graphics.Dispose();
          break;
        case ImageUtility.ResizeType.CropSquare:
          image3 = (Image) new Bitmap(parameters.SelectedWidth, parameters.SelectedHeight);
          graphics = Graphics.FromImage(image3);
          graphics.SmoothingMode = SmoothingMode.AntiAlias;
          graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
          graphics.FillRectangle(Brushes.White, 0, 0, parameters.SelectedWidth, parameters.SelectedHeight);
          graphics.DrawImage(image1, new Rectangle(0, 0, parameters.SelectedWidth, parameters.SelectedHeight), parameters.X, parameters.Y, image3.Width, image3.Height, GraphicsUnit.Pixel);
          graphics = Graphics.FromImage(image2);
          graphics.DrawImage(image3, 0, 0, parameters.Width, parameters.Height);
          graphics.Dispose();
          break;
      }
      EncoderParameter encoderParameter = new EncoderParameter(Encoder.Quality, 90L);
      EncoderParameters encoderParams = new EncoderParameters(1);
      encoderParams.Param[0] = encoderParameter;
      image2.Save((Stream) memoryStream, ImageUtility.GetEncoder(parameters.Format), encoderParams);
    }
    else
    {
      image.Seek(0L, SeekOrigin.Begin);
      image.CopyTo((Stream) memoryStream);
    }
    memoryStream.Seek(0L, SeekOrigin.Begin);
    return (Stream) memoryStream;
  }
  finally
  {
    image1.Dispose();
    if (image2 != null)
      image2.Dispose();
    if (image3 != null)
      image3.Dispose();
    if (graphics != null)
      graphics.Dispose();
  }
}
Michael
  • 4,010
  • 4
  • 28
  • 49
  • It's hard to tell without seeing your `Upload` function implementation. Also pasting the precise expcetion you are getting along with the full stacktrace might help. – Darin Dimitrov Mar 02 '14 at 23:16
  • You should post the graphics related code. Generally, graphics operations can not be multithreaded well, due to a limitation of GDI+, though that shouldn't cause corruption. – Rotem Mar 02 '14 at 23:16
  • Also see this answer http://stackoverflow.com/a/3764923/860585 – Rotem Mar 02 '14 at 23:21
  • @Rotem - reading that other post, it does appear to be the same exact issue. Disappointing... – Michael Mar 03 '14 at 03:54
  • @Michael, you may want to try [this approach](http://stackoverflow.com/q/20993007/1768303) to maintain thread affinity for GDI+ objects, it may actually help. – noseratio Mar 03 '14 at 08:21
  • @Noseratio I'm no threading expert, and I didn't mention that this was in the context of ASP.NET (big omission on my part), but I *believe* the fix you're proposing would eliminate the benefit I'm trying to gain with async - namely scaling the ASP.NET thread pool. I was however struck by one of the commenters on that other SO thread who mentioned thread-safety and static. I mentioned in my question that the crashing method is static and I'm thinking now that I should try re-implementing as non-static. – Michael Mar 03 '14 at 15:41
  • [System.Drawing namespace](http://msdn.microsoft.com/en-us/library/System.Drawing(v=vs.110).aspx): "Classes within the System.Drawing namespace are not supported for use within a Windows or ASP.NET service. Attempting to use these classes from within one of these application types may produce unexpected problems, such as diminished service performance and run-time exceptions. For a supported alternative, see Windows Imaging Components." – Damien_The_Unbeliever Mar 03 '14 at 15:53
  • @Michael, besides the relevant point by @Damien_The_Unbeliever, using `ThreadPool` within the same HTTP request in ASP.NET often kills the scalability of the web app. Basically, you preoccupy some pool threads for CPU-bound image rendering work, while they could otherwise be serving other incoming requests. – noseratio Mar 03 '14 at 22:09
  • @Noseratio Yes, but ideally you could use ConfigureAwait(false) to get around that, right? – Michael Mar 03 '14 at 22:20
  • @Michael, no, it won't get around that. Is your goal to finish the image processing within the *same* HTTP request? If so, your offloaded work items will *have* to finish before you send the response down to the client. So it *may* speed up things for one particular client, but it also *may* be depriving others. If however you want to span tasks across the boundaries of a single HTTP request, check [this](http://stackoverflow.com/a/21873422/1768303). – noseratio Mar 03 '14 at 22:31
  • @Noseratio No - it seemed that running 5 of these, in serial, all while the Request waits, was bad. I thought I could run them in parallel - on non-ASP.NET threads with ConfigureAwait(false) - and finish the processing quicker. Spinning off to a processing service is probably too much so, if I can't do it with TPL (on non-ASP.NET threads) - then I'll probably just run it synchronously. Why doesn't ConfigureAwait(false) fit the bill? – Michael Mar 03 '14 at 22:42
  • `ConfigureAwait(false)` does not eliminate the fact that you're still using an extra thread from the pool. All it does is that the code after `await` will *not* continue on the original `AspNetSynchronizationContext` context, which is a bad idea. I can't see how this may help to address the scalability or any other problem. – noseratio Mar 03 '14 at 22:47
  • @Noseratio This [post](http://stackoverflow.com/questions/10004697/calling-configureawait-from-an-asp-net-mvc-action) seems to indicate no down side. The upside (I thought) was that the special (and more limited) web-request servicing threads would be freed-up while the less-special, more abundant, regular threads processed the images (again, with no apparent issue). – Michael Mar 03 '14 at 22:59
  • @Michael, *"The upside (I thought) was that the special (and more limited) web-request servicing threads would be freed-up while the less-special, more abundant, regular threads processed the images."* This is wrong thinking. I suggest you ask this as a separate question if you're looking for a detailed explanation why. We're going off-topic here. – noseratio Mar 04 '14 at 00:17

0 Answers0