0

我正在尝试写入 aWritableBitmap并且我想在非 UI 线程中进行数据处理。所以我从 UI 调度程序调用LockandUnlock方法,其余的在不同的线程上完成:

IntPtr pBackBuffer = IntPtr.Zero;

Application.Current.Dispatcher.Invoke(new Action(() =>
    {
        Debug.WriteLine("{1}: Begin Image Update: {0}", DateTime.Now, this.GetHashCode());

        _mappedBitmap.Lock();

        pBackBuffer = _mappedBitmap.BackBuffer;
    }));

// Long processing straight on pBackBuffer...

Application.Current.Dispatcher.Invoke(new Action(()=>
{
        Debug.WriteLine("{1}: End Image Update: {0}", DateTime.Now, this.GetHashCode());

        // the entire bitmap has changed
        _mappedBitmap.AddDirtyRect(new Int32Rect(0, 0, _mappedBitmap.PixelWidth,
                                                    _mappedBitmap.PixelHeight));

        // release the back buffer and make it available for display
        _mappedBitmap.Unlock();
}));

可以从任何线程调用此代码,因为它会在需要时专门调用 UI 调度程序。当我的控制压力不大时,这很有效。但是当我几乎立即每 100 毫秒调用一次时,我会收到InvalidOperationException来自AddDirtyRect以下消息的消息:

{"图像解锁时无法调用此方法。"}

我不明白这怎么会发生。我的调试输出日志显示,Lock我的班级的这个实例确实被调用了。

更新

Image我的整个场景:我正在编写一个允许在 WPF控件中显示浮点矩阵的类。该类FloatingPointImageSourceAdapter允许使用 API 设置数据

void SetData(float[] data, int width, int height)

它公开了ImageSource一个Image控件Souce属性可以绑定到的。

在内部,这是使用WritableBitmap. 每当用户设置新数据时,我都需要处理像素并将它们重写到缓冲区中。数据计划以高频率设置,这就是为什么我直接写入BackBuffer而不是调用WritePixels. 此外,由于像素的重新映射可能需要一段时间并且图像可能非常大,我想在单独的线程上进行处理。

我决定通过丢帧来应对高压力。所以我有一个AutoResetEvent跟踪用户何时请求更新数据的方法。我有一个后台任务来完成实际工作。

class FloatingPointImageSourceAdapter
{
    private readonly AutoResetEvent _updateRequired = new AutoResetEvent(false);

    public FloatingPointImageSourceAdapter()
    {
        // all sorts of initializations

        Task.Factory.StartNew(UpdateImage, TaskCreationOptions.LongRunning);
    }

    public void SetData(float[] data, int width, int height)
    {
        // save the data

        _updateRequired.Set();
    }

    private void UpdateImage()
    {
        while (true)
        {
            _updateRequired.WaitOne();

            Debug.WriteLine("{1}: Update requested from thread {2}, {0}", DateTime.Now, this.GetHashCode(), Thread.CurrentThread.ManagedThreadId);

            IntPtr pBackBuffer = IntPtr.Zero;

            Application.Current.Dispatcher.Invoke(new Action(() =>
                {
                    Debug.WriteLine("{1}: Begin Image Update: {0}", DateTime.Now, this.GetHashCode());

                    _mappedBitmap.Lock();

                    pBackBuffer = _mappedBitmap.BackBuffer;
                }));

            // The processing of the back buffer

            Application.Current.Dispatcher.Invoke(new Action(() =>
            {
                Debug.WriteLine("{1}: End Image Update: {0}", DateTime.Now, this.GetHashCode());

                // the entire bitmap has changed
                _mappedBitmap.AddDirtyRect(new Int32Rect(0, 0, _mappedBitmap.PixelWidth,
                                                            _mappedBitmap.PixelHeight));

                // release the back buffer and make it available for display
                _mappedBitmap.Unlock();
            }));
        }
    }
}

为了勇敢起见,我在这里删除了很多代码。

我的测试创建了一个SetData在特定时间间隔内调用的任务:

private void Button_Click_StartStressTest(object sender, RoutedEventArgs e)
{
    var sleepTime = SleepTime;

    _cts = new CancellationTokenSource();

    var ct = _cts.Token;

    for (int i = 0; i < ThreadsNumber; ++i)
    {
        Task.Factory.StartNew(() =>
        {
            while (true)
            {
                if (ct.IsCancellationRequested)
                {
                    break;
                }

                int width = RandomGenerator.Next(10, 1024);
                int height = RandomGenerator.Next(10, 1024);

                var r = new Random((int)DateTime.Now.TimeOfDay.TotalMilliseconds);
                var data = Enumerable.Range(0, width * height).Select(x => (float)r.NextDouble()).ToArray();

                this.BeginInvokeInDispatcherThread(() => FloatingPointImageSource.SetData(data, width, height));

                Thread.Sleep(RandomGenerator.Next((int)(sleepTime * 0.9), (int)(sleepTime * 1.1)));
            }
        }, _cts.Token);
    }
}

ThreadsNumber=1我使用and with运行此测试,SleepTime=100它因上述异常而崩溃。

更新 2

我尝试检查我的命令是否确实是串行执行的。我添加了另一个私有字段

private int _lockCounter;

我在我的while循环中操纵它:

private void UpdateImage()
{
    while (true)
    {
        _updateRequired.WaitOne();

        Debug.Assert(_lockCounter == 0);
        _lockCounter++;

        IntPtr pBackBuffer = IntPtr.Zero;

        Application.Current.Dispatcher.Invoke(new Action(() =>
            {
                Debug.Assert(_lockCounter == 1);
                ++_lockCounter;

                _mappedBitmap.Lock();

                pBackBuffer = _mappedBitmap.BackBuffer;
            }));

        Debug.Assert(pBackBuffer != IntPtr.Zero);

        Debug.Assert(_lockCounter == 2);

        ++_lockCounter;

        // Process back buffer

        Debug.Assert(_lockCounter == 3);
        ++_lockCounter;

        Application.Current.Dispatcher.Invoke(new Action(() =>
        {
            Debug.Assert(_lockCounter == 4);
            ++_lockCounter;

            // the entire bitmap has changed
            _mappedBitmap.AddDirtyRect(new Int32Rect(0, 0, _mappedBitmap.PixelWidth,
                                                        _mappedBitmap.PixelHeight));

            // release the back buffer and make it available for display
            _mappedBitmap.Unlock();


        }));

        Debug.Assert(_lockCounter == 5);
        _lockCounter = 0;
    }
}

我希望如果消息顺序以某种方式搞砸了,我Debug.Assert的 s 会抓住这个。但是柜台的一切都很好。它们根据串行逻辑正确递增,但我仍然从AddDirtyRect.

4

2 回答 2

1

Application.Current.Dispatcher.Invoke 将尝试执行作为委托传递给 UI 线程本身的方法,这将在 UI 线程空闲时发生。如果您尝试连续执行此指令,则几乎不会像在 UI 线程上执行操作一样。始终在 Application.Current.Dispatcher.Invoke 上执行的指令应该非常少,说它应该只有一行,它只会改变 UI 上的值,仅此而已。因此,请避免作为 Dispatcher 的一部分执行的复杂操作,将其移出 Dispatcher 并仅执行更新 UI 的操作

于 2013-10-27T09:00:32.577 回答
0

所以经过一些(很长的)挖掘,结果发现真正的错误隐藏在我为了勇敢而遗漏的代码中:-)

我的班级允许更改图像的大小。设置数据时,我检查新大小是否与旧大小相同,如果不是,我会初始化一个新的WritableBitmap.

发生的事情是图像的大小在while循环中间的某个时间发生了变化(使用不同的线程)。并且这导致了处理代码的不同阶段要处理不同的实例_mappedBitmap(因为_mappedBitmap在不同的阶段指向不同的实例)。因此,当实例更改为新实例时,它是在解锁状态下创建的,从而导致(正当的)异常。

于 2013-10-27T12:08:22.670 回答