Parallel.For语句return"System.InvalidOperationException"带位图处理

Parallel.For statement return "System.InvalidOperationException" with a Bitmap Processing

好吧,我有一个代码可以在 "x" 图像中应用 Rain Bow 滤镜,我必须通过两种方式进行:顺序和并行,我的顺序代码工作没有问题,但并行部分没有工作。我不知道,为什么?

代码

public static Bitmap RainbowFilterParallel(Bitmap bmp)
    {

        Bitmap temp = new Bitmap(bmp.Width, bmp.Height);
        int raz = bmp.Height / 4;

        Parallel.For(0, bmp.Width, i =>
        {
            Parallel.For(0, bmp.Height, x =>
            {

                if (i < (raz))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R / 5, bmp.GetPixel(i, x).G, bmp.GetPixel(i, x).B));
                }
                else if (i < (raz * 2))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R, bmp.GetPixel(i, x).G / 5, bmp.GetPixel(i, x).B));
                }
                else if (i < (raz * 3))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R, bmp.GetPixel(i, x).G, bmp.GetPixel(i, x).B / 5));
                }
                else if (i < (raz * 4))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R / 5, bmp.GetPixel(i, x).G, bmp.GetPixel(i, x).B / 5));
                }
                else
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R / 5, bmp.GetPixel(i, x).G / 5, bmp.GetPixel(i, x).B / 5));
                }

            });

        });
        return temp;
    }

此外,稍后程序 return 同样的错误但是说 "The object is already in use"。

PS。我是 c# 初学者,我在另一个 post 中搜索了这个主题,但我什么也没找到。

非常感谢您

正如评论者 Ron Beyer 指出的那样,使用 SetPixel()GetPixel() 方法非常慢。对这些方法之一的每次调用都涉及 lot 在托管代码到 Bitmap 对象表示的实际二进制缓冲区之间的转换过程中的开销。那里有很多层,通常涉及视频驱动程序,这需要在用户级和内核级执行之间进行转换。

但是除了速度慢之外,这些方法还会生成对象 "busy",因此如果在调用其中一个方法期间尝试使用位图(包括调用其中一个方法)当它 returns 时(即当呼叫正在进行时),发生错误,出现异常。

因为并行化当前代码的唯一方法是如果这些方法调用可以并发发生,并且由于它们根本不能同时发生,所以这种方法行不通。

另一方面,使用 LockBits() 方法不仅可以保证有效,而且很有可能您会发现使用 LockBits() 的性能比您不使用的要好得多。甚至不需要并行化算法。但是如果你决定这样做,因为 LockBits() 的工作方式——你可以访问代表位图图像的原始字节缓冲区——你可以轻松地并行化算法并利用多个 CPU 核心(如果存在)。

请注意,在使用 LockBits() 时,您将以您可能不习惯的级别使用 Bitmap 对象。如果您还不了解位图的实际工作原理 "under the hood",您必须熟悉位图实际存储在内存中的方式。这包括了解不同像素格式的含义,如何解释和修改给定格式的像素,以及位图在内存中的布局方式(例如,行的顺序可能因位图而异,以及 "stride"位图)。

这些东西学起来并不难,但需要耐心。如果性能是您的目标,那么付出努力是值得的。

并行对于单一的头脑来说很难。将它与遗留 GDI+ 代码混合会导致奇怪的结果..

您的代码有很多问题:

  • 每个像素调用 GetPixel 三次而不是一次
  • 您没有按照应有的方式水平访问像素
  • 你称y x 和x i;机器不会介意,但我们人会介意
  • 方式使用了过多的并行化。拥有比拥有核心更多的东西是没有用的。它产生的开销必然会消耗掉任何收益,除非你的内部循环有非常艰巨的工作要做,比如数百万次计算..

但是你得到的异常与这些问题无关。你不会犯的一个错误是并行访问同一个像素......那么为什么会崩溃?

清理代码后,我发现堆栈跟踪中的错误指向 SetPixel,然后指向 System.Drawing.Image.get_Width()。前者很明显,后者不是我们代码的一部分..!?

所以我深入研究了 referencesource.microsoft.com 的源代码,发现了这个:

    /// <include file='doc\Bitmap.uex' path='docs/doc[@for="Bitmap.SetPixel"]/*' />
    /// <devdoc>
    ///    <para>
    ///       Sets the color of the specified pixel in this <see cref='System.Drawing.Bitmap'/> .
    ///    </para>
    /// </devdoc>
    public void SetPixel(int x, int y, Color color) {
        if ((PixelFormat & PixelFormat.Indexed) != 0) {
            throw new InvalidOperationException(SR.GetString(SR.GdiplusCannotSetPixelFromIndexedPixelFormat));
        }

        if (x < 0 || x >= Width) {
            throw new ArgumentOutOfRangeException("x", SR.GetString(SR.ValidRangeX));
        }

        if (y < 0 || y >= Height) {
            throw new ArgumentOutOfRangeException("y", SR.GetString(SR.ValidRangeY));
        }

        int status = SafeNativeMethods.Gdip.GdipBitmapSetPixel(new HandleRef(this, nativeImage), x, y, color.ToArgb());

        if (status != SafeNativeMethods.Gdip.Ok)
            throw SafeNativeMethods.Gdip.StatusException(status);
    }

真正的工作是由 SafeNativeMethods.Gdip.GdipBitmapSetPixel 完成的,但在此之前,该方法会对位图的宽度和高度进行边界检查。虽然在我们的例子中这些当然永远不会改变,但系统仍然不允许并行访问它们,因此在某些时候检查交织在一起时会崩溃。当然,完全没有必要,但是你去..

因此 GetPixel(具有相同的行为)和 SetPixel 不能安全地用于并行处理。

两种解决方法:

我们可以将 locks 添加到代码中,从而确保检查不会在 'same' 时间发生:

public static Bitmap RainbowFilterParallel(Bitmap bmp)
{
    Bitmap temp = new Bitmap(bmp);
    int raz = bmp.Height / 4;
    int height = bmp.Height;
    int width = bmp.Width;
    // set a limit to parallesim
    int maxCore = 7;
    int blockH = height / maxCore + 1;

    //lock (temp) 

    Parallel.For(0, maxCore, cor =>
    {

        //Parallel.For(0, bmp.Height, x =>
        for (int yb = 0; yb < blockH; yb++)
        {
            int i = cor * blockH + yb;
            if (i >= height) continue;
            for (int x = 0; x < width; x++)
            {
                {
                  Color c;
                  // lock the Bitmap just for the GetPixel: 
                  lock (temp) c  = temp.GetPixel(x, i);
                  byte R = c.R;
                  byte G = c.G;
                  byte B = c.B;
                  if (i < (raz)) { R = (byte)(c.R / 5); }
                  else if (i < raz + raz) { G = (byte)(c.G / 5); }
                  else if (i < raz * 3) { B = (byte)(c.B / 5); }
                  else if (i < raz * 4) { R = (byte)(c.R / 5); B = (byte)(c.B / 5); }
                  else { G = (byte)(c.G / 5); R = (byte)(c.R / 5); }
                  // lock the Bitmap just for the SetPixel:
                  lock (temp) temp.SetPixel(x, i, Color.FromArgb(R,G,B));
                };
            }

        };

    });
    return temp;
}

请注意,限制并行度非常重要,甚至 ParallelOptions class and a parameter inParallel.For 中还有一个成员可以控制它!我已将最大核心数设置为 7,但这样会更好:

int degreeOfParallelism = Environment.ProcessorCount - 1;

所以这应该可以为我们节省一些开销。但仍然:我希望它比更正的顺序方法慢!

而不是像 Peter 和 Ron 所建议的那样使用 LockBits 方法使事情变得非常快 (1ox) 并且添加并行性可能甚至更快..

所以最后要完成这个冗长的答案,这里是一个 Lockbits 加有限并行解决方案:

public static Bitmap RainbowFilterParallelLockbits(Bitmap bmp)
{
    Bitmap temp = null;
    temp = new Bitmap(bmp);
    int raz = bmp.Height / 4;
    int height = bmp.Height;
    int width = bmp.Width;
    Rectangle rect = new Rectangle(Point.Empty, bmp.Size);

    BitmapData bmpData = temp.LockBits(rect,ImageLockMode.ReadOnly, temp.PixelFormat);
    int bpp = (temp.PixelFormat == PixelFormat.Format32bppArgb) ? 4 : 3;
    int size = bmpData.Stride * bmpData.Height;
    byte[] data = new byte[size];
    System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, data, 0, size);

     var options = new ParallelOptions();
     int maxCore = Environment.ProcessorCount - 1;
     options.MaxDegreeOfParallelism = maxCore > 0 ? maxCore  : 1;

    Parallel.For(0, height, options, y =>
    {
        for (int x = 0; x < width; x++)
        {
            {
              int index = y * bmpData.Stride + x * bpp;

              if (y < (raz))   data[index + 2] = (byte) (data[index + 2] / 5);
              else if (y < (raz * 2))  data[index + 1] = (byte)(data[index + 1] / 5);
              else if (y < (raz * 3)) data[index ] = (byte)(data[index ] / 5);
              else if (y < (raz * 4))
                {   data[index + 2] = (byte)(data[index + 2] / 5); 
                    data[index] = (byte)(data[index] / 5); }
              else
              {   data[index + 2] = (byte)(data[index + 2] / 5);
                  data[index + 1] = (byte)(data[index + 1] / 5);
                  data[index] = (byte)(data[index] / 5);  }
          };
        };

    });
    System.Runtime.InteropServices.Marshal.Copy(data, 0, bmpData.Scan0, data.Length);
    temp.UnlockBits(bmpData);
    return temp;
}