【问题标题】:C# asynchronous LCD writeC#异步LCD写
【发布时间】:2019-10-15 20:23:08
【问题描述】:

所以我正在做一个涉及每秒可更新 60 次的 LCD 屏幕的项目。它使用BitmapFrame,我需要将这些像素复制到更新屏幕的库中。目前我得到大约 30-35 FPS,这太低了。所以我尝试使用多线程,但这会产生很多问题。

DisplayController 已经创建了一个 thead 来完成所有工作,如下所示:

public void Start()
{
    _looper = new Thread(Loop);
    _looper.IsBackground = true;
    _looper.Start();
}

private void Loop()
{
    while (_IsRunning)
    {
        renderScreen();
    }
}

调用renderScreen 方法绘制所有元素并将像素复制到BitmapFrame。但是这个过程需要太长时间,所以我的 FPS 下降了。我试图解决这个问题是通过创建一个Task 来绘制、复制和写入像素。但是这种解决方案会占用大量 CPU 并导致屏幕出现故障。

public void renderScreen()
{
    Task.Run(() =>
    {
        Monitor.Enter(_object);

        // Push screen to LCD
        BitmapFrame bf = BitmapFrame.Create(screen);
        RenderOptions.SetBitmapScalingMode(bf, BitmapScalingMode.LowQuality);
        bf.CopyPixels(new Int32Rect(0, 0, width, height), pixels, width * 4, 0);

        DisplayWrapper.USBD480_DrawFullScreenBGRA32(ref disp, pixels);

        Monitor.Exit(_object);
    });
}

我已经阅读了很多有关 C# 并发队列的信息,但这不是我需要的。并且使用两个线程会导致编译器说该变量由另一个线程拥有的问题。

如何同时渲染一个新位图并将该位图每秒写入 LCD 60 次?

【问题讨论】:

  • 你的循环方法是一个无限循环,它创建并启动一个任务而不等待它完成。所以它会像每秒 1000 个任务或线程一样开始。当然,这会减慢一切,即使所有这些都在您的监视器锁定处等待,因为不断创建新任务/线程。删除 Task.Run 和 Monitor 锁。
  • @ckuri 那是我以前的版本。这导致应用程序太慢,大约 30 FPS。这就是我尝试使用 Task.Run 的原因。但是你说得对的无限循环创建部分是有道理的。

标签: c# .net wpf asynchronous concurrency


【解决方案1】:

我假设 USBD480_DrawFullScreenBGRA32 是实际写入 LCD 的内容,其余代码只是准备图像。我认为您提高性能的关键是准备下一张图片同时上一张图片正在写入。

我认为您最好的解决方案是使用两个线程并使用ConcurrentQueue 作为需要写入的缓冲区。一个线程准备图像和puts them into the ConcurrentQueue,另一个线程pulls them off the queue 并将它们写入LCD。这样您就不必每次都调用Task.Run

限制写入队列的帧数可能也是明智之举,这样它就不会超前并占用不必要的内存。

【讨论】:

  • 我同意,Task.Run 可能不需要。
  • @tymtam 是的,现在我想多了,我刚刚删除了Task.Run 代码。这两个线程确实是最好的方法。
  • 当生产者比展示者快的情况下,你会如何处理?你不会成长和成长(并显示旧图像)吗?
  • 我假设每个队列项目都将保留其图像副本。在 60fps 时,这不是一个微不足道的分配数量。你怎么看?
  • 这是我最后添加的部分 :) 限制缓冲区的大小是个好主意。
【解决方案2】:

我认为你应该有两个线程(并且只有两个):

  1. 连续创建位图;和
  2. 一种持续获取最新位图并将其推送到 LCD 的设备。

这是我的幼稚实现。

我使用了一个包含最新生成的图像的共享数组,因为它使分配数量保持在较低水平。一个共享数组,我们可以摆脱 3 个数组对象(共享 + 2 个线程本地)。

public class Program
{
    public class A
    {
        private readonly object pixelsLock = new object();

        Array shared = ...;

        public void Method2()
        {
            Array myPixels = (...);
            while (true)
            {
                // Prepare image
                BitmapFrame bf = BitmapFrame.Create(screen);
                RenderOptions.SetBitmapScalingMode(bf, BitmapScalingMode.LowQuality);
                bf.CopyPixels(new Int32Rect(0, 0, width, height), myPixels, width * 4, 0);

                lock (pixelsLock)
                {
                    // Copy the hard work to shared storage
                    Array.Copy(sourceArray: myPixels, destinationArray: shared, length: myPixels.GetUpperBound(0) - 1);
                }
            }
        }

        public void Method1()
        {
            Array myPixels = (...);
            while (true)
            {
                lock (pixelsLock)
                {
                    //Max a local copy
                    Array.Copy(sourceArray: shared, destinationArray: myPixels, length: myPixels.GetUpperBound(0) - 1);
                }
                DisplayWrapper.USBD480_DrawFullScreenBGRA32(ref disp, myPixels);
            }
        }
    }


    public static async Task Main(string[] args)
    {
        var a = new A();
        new Thread(new ThreadStart(a.Method1)).Start();
        new Thread(new ThreadStart(a.Method2)).Start();
        Console.ReadLine();
    }
}

【讨论】:

  • 您使用像素作为共享内存的想法是个好主意。我试图使用 screen 参数,但在从不同线程写入屏幕时会产生问题。如何解决Array.Copy这个线程所有权问题?为什么这有效,而不是我的类中的静态参数(也使用锁)?
  • 拥有一个共享数组专用来交换数据意味着唯一需要锁定的Array.Copy 快速。这意味着线程相互阻塞的频率更低,时间更短。
【解决方案3】:

您可以考虑使用强大、高性能且高度可配置的TPL Dataflow library,这将允许您构建数据管道。您会将原始数据发布到管道的第一个块中,并且数据将在从一个块流向下一个块时进行转换,然后最终在最后一个块处呈现。所有块将并行工作。在下面的示例中,共有三个块,都配置了默认的MaxDegreeOfParallelism = 1,因此最多有 3 个线程同时忙于工作。我有意为这些块配置了一个较小的BoundedCapacity,这样如果传入的原始数据超过管道可以处理的数据,就会丢弃过多的输入。

var block1 = new TransformBlock<Stream, BitmapFrame>(stream =>
{
    BitmapFrame bf = BitmapFrame.Create(stream);
    RenderOptions.SetBitmapScalingMode(bf, BitmapScalingMode.LowQuality);
    return bf;
}, new ExecutionDataflowBlockOptions()
{
    BoundedCapacity = 5
});

var block2 = new TransformBlock<BitmapFrame, int[]>(bf =>
{
    var pixels = new int[width * height * 4];
    bf.CopyPixels(new Int32Rect(0, 0, width, height), pixels, width * 4, 0);
    return pixels;
}, new ExecutionDataflowBlockOptions()
{
    BoundedCapacity = 5
});

var block3 = new ActionBlock<int[]>(pixels =>
{
    DisplayWrapper.USBD480_DrawFullScreenBGRA32(ref disp, pixels);
}, new ExecutionDataflowBlockOptions()
{
    BoundedCapacity = 5
});

管道是通过将块链接在一起来创建的:

block1.LinkTo(block2, new DataflowLinkOptions() { PropagateCompletion = true });
block2.LinkTo(block3, new DataflowLinkOptions() { PropagateCompletion = true });

最后循环采用以下形式:

void Loop()
{
    while (_IsRunning)
    {
        block1.Post(GetRawStreamData());
    }
    block1.Complete();
    block3.Completion.Wait(); // Optional, to wait for the last data to be processed
}

在这个例子中,使用了两种类型的块,两种TransformBlocks,最后一种ActionBlockActionBlocks 不会产生任何输出,因此它们经常出现在 TPL 数据流管道的末端。

TPL Dataflow 的替代方案是最近推出的名为 Channels 的库,这是一个易于学习的小型库。这包括有趣的选项BoundedChannelFullMode,用于选择队列已满时丢弃哪些项目:

DropNewest:删除并忽略频道中的最新项目,以便为正在写入的项目腾出空间。
DropOldest:删除并忽略最旧的项目在频道中,以便为正在写入的项目腾出空间。
DropWrite:放下正在写入的项目。
等待:等待空间可用以完成写操作。

相比之下,TPL Dataflow 只有两个选项。它可以使用演示的block1.Post(...) 删除正在写入的项目,或者使用替代的block1.SendAsync(...).Wait() 等待空间可用。

但通道并不是 TPL 数据流的完全替代品,因为它们只处理工作项的排队,而不是它们的实际处理。

【讨论】:

  • 嗨 Theodor,非常感谢您的详细解释。这与双线程解决方案(内存/cpu 方面)相比如何?
  • @Leon 它的性能可能至少与手动线程编码解决方案相同,甚至可能更好。这取决于工作负载的平衡程度。例如,如果最后一步(渲染)是最慢的一步,那么性能将是相同的,因为这两种方法都将受到同一瓶颈的限制,这是不可并行的。如果第二步是最慢的一步,那么使用 TPL 数据流,您可以选择增加此块的并行度 (MaxDegreeOfParallelism = 2) 并获得更好的性能。不过,我不认为改进会令人印象深刻。
  • 内存方面取决于BoundedCapacity 设置。例如,如果将所有块的值设置为 1,则位图帧的最多 3 个内存表示将同时存储在内存中,每个块一个。可以配置,应该问题不大,除非系统内存资源极其有限。
  • @Leon 顺便说一句,您可能应该尝试优化进料循环。目前,您有一个完全消耗一个 CPU 内核的紧密循环,并且可能多次向管道提供相同的原始数据。
  • @Leon 你没事吧。这段小代码一定很聪明! :-)
猜你喜欢
  • 2018-07-26
  • 2011-09-02
  • 2014-01-11
  • 1970-01-01
  • 1970-01-01
  • 2011-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多