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;
}
好吧,我有一个代码可以在 "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;
}