InvalidOperationException - 对象当前正在其他地方使用 - 红十字标志

30

我有一个 C# 桌面应用程序,其中我创建了一个线程,不断从一个源(实际上是一个数字相机)获取图像,并将其放在 GUI 中的一个面板上(panel.Image = img),此面板必须是另一个线程,因为它是控件的代码后端。

该应用程序可以正常工作,但在某些机器上,我会在随机时间间隔(不可预测)收到以下错误:

************** Exception Text **************
System.InvalidOperationException: The object is currently in use elsewhere. 
然后面板变成了一个红十字,红色X - 我想这是可以从属性中编辑的无效图片图标。应用程序继续工作,但面板永远不会更新。
据我所知,这个错误来自控件的onpaint事件,在那里我在图片上画了其他东西。
我尝试在那里使用锁定,但没有运气 :(
我调用将图像放在面板上的函数的方式如下:
if (this.ReceivedFrame != null)
{
    Delegate[] clients = this.ReceivedFrame.GetInvocationList();
    foreach (Delegate del in clients)
    {
        try
        {
            del.DynamicInvoke(new object[] { this, 
                new StreamEventArgs(frame)} );
        }
        catch { }
    }
}

这是委托:

public delegate void ReceivedFrameEventHandler(object sender, StreamEventArgs e);
    public event ReceivedFrameEventHandler ReceivedFrame;

以下是控件代码后台中函数如何注册到它的方法:

Camera.ReceivedFrame += 
    new Camera.ReceivedFrameEventHandler(camera_ReceivedFrame);

我也尝试了

del.Method.Invoke(del.Target, new object[] { this, new StreamEventArgs(b) });

而不是

del.DynamicInvoke(new object[] { this, new StreamEventArgs(frame) });

但是没有运气

有人知道我如何修复这个错误或者至少如何捕获错误并让线程再次将图像放在面板上吗?

4个回答

20

这是因为Gdi+ Image类不是线程安全的。然而,你可以通过在访问图像时每次使用锁来避免InvalidOperationException的出现,比如用于绘画或获取图像大小:

Image DummyImage;

// Paint
lock (DummyImage)
    e.Graphics.DrawImage(DummyImage, 10, 10);

// Access Image properties
Size ImageSize;
lock (DummyImage)
    ImageSize = DummyImage.Size;

顺便提一下,如果你使用上述模式,则无需调用该函数。


我的函数在onpaint事件中运行,会在图像上方的面板上绘制很多东西,而另一个线程设置在该面板上的图像。那么我该如何锁定所有绘制的内容?这包括矩形、线条和图像。 - para
我尝试锁定面板,但它没有起作用,我仍然收到了错误。 - para
你不需要锁定面板,而是需要锁定你正在处理的特定图像。 - arbiter
我想知道如果他调用LockBits(),然后在中间进行渲染,最后再调用UnlockBits()会发生什么。 - Dmitri Nesteruk

5

我遇到了同样的问题,出现了相同的错误信息,但是无论我如何尝试,锁定位图都不能解决问题。后来我发现,我正在使用一个静态画刷绘制形状。果然,正是画刷导致了线程争用。

var location = new Rectangle(100, 100, 500, 500);
var brush = MyClass.RED_BRUSH;
lock(brush)
    e.Graphics.FillRectangle(brush, location);

这对我的情况起作用,我学到了一个教训:在发生线程争用的位置检查所有正在使用的引用类型。

1
在我的情况下也是画笔。只是为了好玩,我尝试使用字体,但似乎可以从多个线程工作。 - Alois Kraus

2

在我看来,同一个相机对象被多次使用。

例如,尝试为每个接收到的帧使用新的缓冲区。在图片框绘制新帧时,捕获库会再次填充该缓冲区。因此,在较快的机器上可能不是问题,在较慢的机器上可能会是问题。

我曾经编写过类似的程序,每收到一帧,我们必须请求接收下一帧并设置该请求中的NEW帧接收缓冲区。

如果你不能这样做,请先将从相机接收到的帧复制到新缓冲区中,并将该缓冲区附加到队列中,或者只使用两个交替缓冲区并检查是否溢出。要么使用myOutPutPanel.BeginInvoke调用camera_ReceivedFrame方法,要么最好有一个线程在运行,检查队列,当它有新条目时,调用mnyOutPutPanel.BeginInvoke来调用您的方法,在面板上设置新缓冲区为图像。

此外,一旦您接收到缓冲区,请使用Panel Invoke方法调用图像设置(确保它在窗口线程中而不是您的捕获库线程中运行)。

以下示例可以从任何线程(捕获库或其他单独线程)调用:

void camera_ReceivedFrame(object sender, StreamEventArgs e)
{
    if(myOutputPanel.InvokeRequired)
    {
        myOutPutPanel.BeginInvoke( 
            new Camera.ReceivedFrameEventHandler(camera_ReceivedFrame), 
            sender, 
            e);
    }
    else
    {
        myOutPutPanel.Image = e.Image;
    }
}

0

我认为这是一个多线程问题。 使用Windows黄金法则,在主线程中更新面板,使用panel.Invoke。 这应该可以解决跨线程异常。


我正在主线程中更新面板,但我从另一个线程调用更新函数并将图像作为参数传递。 - para
如果您从另一个线程调用更新面板的函数,并且在函数本身中没有上下文切换(例如使用invoke切换到主线程),则意味着您的更新是在其他线程而不是主线程上完成的。 - Ahmed

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