访问冲突异常之谜

23

我已经与EMGU+OpenCV合作相当长一段时间,并遇到了这个AccessViolationException神秘事件。

首先是代码:

class AVE_Simulation
    {
        public static int Width = 7500;
        public static int Height = 7500;
        public static Emgu.CV.Image<Rgb, float>[] Images;

        static void Main(string[] args)
        {
            int N = 50;
            int Threads = 5;

            Images = new Emgu.CV.Image<Rgb, float>[N];
            Console.WriteLine("Start");

            ParallelOptions po = new ParallelOptions();
            po.MaxDegreeOfParallelism = Threads;
            System.Threading.Tasks.Parallel.For(0, N, po, new Action<int>((i) =>
            {
                Images[i] = GetRandomImage();
                Console.WriteLine("Prossing image: " + i);
                Images[i].SmoothBilatral(15, 50, 50);
                GC.Collect();
            }));
            Console.WriteLine("End");
        }

        public static Emgu.CV.Image<Rgb, float> GetRandomImage()
        {
            Emgu.CV.Image<Rgb, float> im = new Emgu.CV.Image<Rgb, float>(Width, Height);

            float[, ,] d = im.Data;
            Random r = new Random((int)DateTime.Now.Ticks);

            for (int y = 0; y < Height; y++)
            {
                for (int x = 0; x < Width; x++)
                {
                    d[y, x, 0] = (float)r.Next(255);
                    d[y, x, 1] = (float)r.Next(255);
                    d[y, x, 2] = (float)r.Next(255);
                }
            }
            return im;
        }

    }
代码很简单。分配一个图像数组。生成一个随机图像并填充随机数。在该图像上执行双边滤波器。就这些。
如果我在单个线程中执行此程序(Threads=1),则一切正常,没有问题。但是,如果我将并发线程数提高到5,则很快就会出现AccessViolationException的异常。
我查看了OpenCV代码,并验证了OpenCV侧面没有进行任何分配,也查看了EMGU代码以搜索未固定对象或其他问题,一切似乎都正确。
一些注意事项:
1. 如果删除GC.Collect(),则AccessViolationException的异常会更少,但最终仍会发生。 2. 这仅在Release模式下发生。在Debug模式下,我没有遇到任何异常。 3. 虽然每个图像为675MB,但分配没有问题(我有很多内存),如果系统内存不足,则会抛出OutOfMemoryException异常。 4. 我使用双边滤波器,但我也使用其他滤波器/函数时出现这种异常。
感谢任何帮助。我已经试图修复这个问题已经超过一个星期了。
i7(无超频),Win7 64位,32GB RAM,VS 2010,Framework 4.0,OpenCV 2.4.3
栈:
Start
Prossing image: 20
Prossing image: 30
Prossing image: 40
Prossing image: 0
Prossing image: 10
Prossing image: 21

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at Emgu.CV.CvInvoke.cvSmooth(IntPtr src, IntPtr dst, SMOOTH_TYPE type, Int32 param1, Int32 param2, Double param3, Double param4)
   at TestMemoryViolationCrash.AVE_Simulation.<Main>b__0(Int32 i) in C:\branches\1.1\TestMemoryViolationCrash\Program.cs:line 32
   at System.Threading.Tasks.Parallel.<>c__DisplayClassf`1.<ForWorker>b__c()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass10.<ExecuteSelfReplicating>b__f(Object param0)
   at System.Threading.Tasks.Task.Execute()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
   at System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
   at System.Threading.Tasks.ThreadPoolTaskScheduler.TryExecuteTaskInline(Task task, Boolean taskWasPreviouslyQueued)
   at System.Threading.Tasks.TaskScheduler.TryRunInline(Task task, Boolean taskWasPreviouslyQueued)
   at System.Threading.Tasks.Task.InternalRunSynchronously(TaskScheduler scheduler, Boolean waitForCompletion)
   at System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action`1 body, Action`2 bodyWithState, Func`4 bodyWithLocal, Func`1 loc
alInit, Action`1 localFinally)
   at System.Threading.Tasks.Parallel.For(Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action`1 body)
   at TestMemoryViolationCrash.AVE_Simulation.Main(String[] args) in C:\branches\1.1\TestMemoryViolationCrash\Program.cs:line 35

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at Emgu.CV.CvInvoke.cvSmooth(IntPtr src, IntPtr dst, SMOOTH_TYPE type, Int32 param1, Int32 param2, Double param3, Double param4)
   at TestMemoryViolationCrash.AVE_Simulation.<Main>b__0(Int32 i) in C:\branches\1.1\TestMemoryViolationCrash\Program.cs:line 32
   at System.Threading.Tasks.Parallel.<>c__DisplayClassf`1.<ForWorker>b__c()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass10.<ExecuteSelfReplicating>b__f(Object param0)
   at System.Threading.Tasks.Task.Execute()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
   at System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at Emgu.CV.CvInvoke.cvSmooth(IntPtr src, IntPtr dst, SMOOTH_TYPE type, Int32 param1, Int32 param2, Double param3, Double param4)
   at TestMemoryViolationCrash.AVE_Simulation.<Main>b__0(Int32 i) in C:\branches\1.1\TestMemoryViolationCrash\Program.cs:line 32
   at System.Threading.Tasks.Parallel.<>c__DisplayClassf`1.<ForWorker>b__c()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass10.<ExecuteSelfReplicating>b__f(Object param0)
   at System.Threading.Tasks.Task.Execute()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
   at System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
Press any key to continue . . .

当异常发生时,请发布堆栈信息。 - Security Hound
6
我不明白为什么情况如此明显。为什么在“发布模式”下会出现崩溃,而在“调试模式”下却没有?为什么单线程执行时工作正常,而多线程执行时会崩溃?请给我解释一下。 - Gilad
1
@Gilad,我通常遇到这个错误时,要么是尝试读取已释放的图像,要么是多个线程共享同一个图像指针。不过你的代码看起来很直接。 - Asti
2
在名为XXXX的代码片段中出现AccessViolationException(99%的概率)是XXXX中的一个错误。您应该联系Emgu.CV的支持团队。这可能是因为您执行了某些它不喜欢的操作,也许您可以找到解决方法,但仍然是Emgu.CV中的一个错误。 - Simon Mourier
我知道这个案例与此无关,它只是一个例子,展示了OpenCV在多线程环境下并不是完全没有bug的(即使是最新版本也是如此)。 - Simon Mourier
显示剩余7条评论
2个回答

13

你的示例没有保留Image.SmoothBilatral方法产生的结果图像的引用。输入图像保存在静态数组中,因此没有问题。

Emgu.CV图像的数据数组被固定到实际图像内部的GCHandle上,这与图像包含数组并且不阻止在非托管代码使用GCHandle指针时进行收集的事实没有什么区别(在没有对图像进行托管根的情况下)。

因为Image.SmoothBilatral方法除了传递其指针和返回它之外,并没有对其临时结果图像做任何操作,所以我认为它被优化掉了,以至于在平滑处理过程中可以回收结果图像。

因为该类没有终结器,opencv将不会被调用释放其未管理的图像头(该头包含指向托管图像数据的指针),因此opencv仍然认为它有一个可用的图像结构。

你可以通过获取SmoothBilatral方法的结果引用并对其执行某些操作(例如处理)来修复它。

此扩展方法也可以工作(即使结果未被使用,也可以成功调用它进行基准测试):

public static class BilateralExtensionFix
{
    public static Emgu.CV.Image<testchannels, testtype> SmoothBilateral(this Emgu.CV.Image<testchannels, testtype> image, int p1, int p2 , int p3)
    {
        var result = image.CopyBlank();
        var handle = GCHandle.Alloc(result);
        Emgu.CV.CvInvoke.cvSmooth(image.Ptr, result.Ptr, Emgu.CV.CvEnum.SMOOTH_TYPE.CV_BILATERAL, p1, p1, p2, p3);
        handle.Free();
        return result;
    }
}

我认为EmguCV应该只固定指针以便在交互调用时传递给OpenCV。

另外,如果所有通道中的图像浮点值变化为零(min() = max()),则OpenCv双边滤波器会崩溃(产生与您遇到的问题非常相似的错误)。我认为这是因为它如何构建其分组的exp()查找表所致。

可以使用以下方法重现此问题:

    // create new blank image
    var zeroesF1 = new Emgu.CV.Image<Rgb, float>(75, 75);
    // uncomment next line for failure
    zeroesF1.Data[0, 0, 0] += 1.2037063600E-035f;
    zeroesF1.SmoothBilatral(15, 50, 50);

这让我感到困惑,因为实际上有时由于我的测试代码中的一个错误而导致了这个错误...


2
谢谢Gilad。有趣的问题...让我学到了很多我以为自己已经知道的东西。 - Peter Wishart
@PeterWishart 你能否请看一下这个 https://stackoverflow.com/questions/50388992/emgucv-out-of-memory-expcetion-in-x86-release-mode-only-sharpening-images - techno

3
你使用的Emgu CV版本是什么?我找不到2.4.3版本。
很确定你的代码不是问题所在
Emgu CV中的Emgu.CV.Image构造函数可能存在并发问题(无论是在托管包装器还是非托管代码中)。Emgu CV主干中处理托管数据数组的方式似乎很稳定,但在图像构造函数期间分配了一些非托管数据,这可能出了问题。
如果尝试以下操作会发生什么:
  • Images[i] = GetRandomImage();移到parallel For()之外。
  • GetRandomImage()中的Image构造函数周围加上lock()
我注意到有人报告了类似问题的已关闭错误报告(调用图像构造函数并行发生,但图像本身不在线程之间共享)here
[编辑]
是的,这很奇怪。我可以使用库存2.4.2版本和OpenCV二进制文件重现。
如果并行循环中的线程数超过我的核心数,对我来说似乎只会崩溃,我的核心数是>2.. 很有趣,想知道你的测试系统有多少个核心。
此外,仅当代码未附加到调试器且启用了优化代码时才会崩溃 - 您是否曾经在带有调试器附加的发布模式中观察到它?
由于SmoothBilateral函数受CPU限制,使用MaxDegreeOfParallelism超过核心数实际上并没有添加任何好处,因此假设我发现的关于线程数与核心数的问题也适用于您的装置(草率的法则预测:不是)。
所以我的猜测是,在运行JIT优化并且GC正在移动托管数据时,Emgu存在并发/易失性问题,但正如您所说,Emgu代码中没有明显的未固定指向托管对象的指针问题。
虽然我仍然无法正确解释它,但这是我目前发现的:
删除GC.Collect +控制台日志后,对GetRandomImage()的调用进行序列化,并在MSVC之外运行代码,我无法重现该问题(尽管这可能只是减少了频率)。
            public static int Width = 750;
            public static int Height = 750;
...
                int N = 500;
                int Threads = 11;
                Images = new Emgu.CV.Image<Rgb, float>[N];
                Console.WriteLine("Start");
                ParallelOptions po = new ParallelOptions();
                po.MaxDegreeOfParallelism = Threads;
                for (int i = 0; i < N; i++)
                {
                    Images[i] = GetRandomImage();
                }
                System.Threading.Tasks.Parallel.For(0, N, po, new Action<int>((i) =>
                {
                    //Console.WriteLine("CallingSmooth");
                    Images[i].SmoothBilatral(15, 50, 50);
                    //Console.WriteLine("SmoothCompleted");
                }));
                Console.WriteLine("End");

我在并行循环之外添加了一个计时器来触发GC.Collect,但仍比通常情况下更频繁地触发:
        var t = new System.Threading.Timer((dummy) => { 
            GC.Collect(); 
        }, null, 100,100);

通过这个更改,我仍然无法重现该问题,尽管由于线程池繁忙,GC collect的调用不太一致,而且主循环中几乎没有(或非常少)托管分配需要进行收集。取消注释围绕SmoothBilatral调用的控制台日志,然后很快就会出现错误(我想是因为给了GC一些东西来收集)。
[另一个编辑] OpenCV 2.4.2参考手册指出cvSmooth已过时,并且“中值和双边滤波器适用于1通道或3通道8位图像,并且不能原地处理图像。”...并不是很令人鼓舞!
我发现在字节或浮点图像上使用中值滤波器,在字节图像上使用双边滤波器效果很好,我看不出任何CLR / GC问题为什么也不会影响这些情况。
因此,尽管C#测试程序产生了奇怪的效果,我仍然认为这是Emgu / OpenCV的错误。
如果您还没有这样做,应该使用自己编译的opencv二进制文件进行测试,如果仍然失败,请将测试转换为C ++。
请注意,OpenCV有其自己的并行实现,可能会更快。

我使用了EMGU 2.4.2,并对其进行了调整,使其与OpenCV 2.4.3二进制文件配合使用。没有任何破坏性的变化,只是增加了一些方法(和错误修复)。我多次跟踪EMGU代码,似乎它编写得很正确(所有相关对象都被固定)。这就是为什么这个“错误”如此奇怪的原因。 - Gilad
我也检查了“已关闭的错误”。看起来他的解决方案是将进程代码移动到枚举器函数中。我非常确定,当他发现解决方案时,他没有注意到他的代码不再并行运行(我猜这是找到解决方案的肾上腺素飙升的原因)... - Gilad
感谢您花时间阅读这篇文章。我的CPU是4核心带超线程,因此可以同时运行8个图像。问题很明显,即使有些对象被固定,它们也会被GC(执行GC.Collect时)移动。我非常强烈地感觉这是MS框架中的一个错误。我还检查了Framework 4.5(VS2012),并得到了相同的错误。 - Gilad
是的,我确实遇到了崩溃。真正的问题是在一个不同的线程执行带有p/Invoke函数的操作时,GC.Collect被执行了。所以可以想象,这可能会发生在2个线程中。 - Gilad
双边滤波器已从平滑函数中移除,并赋予了自己的函数,详见第225页。它可以接收浮点图像,这是EMGU使用的方法。 - Gilad
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接