繁体   English   中英

C#异步液晶写

[英]C# asynchronous LCD write

所以我正在做一个涉及每秒可以更新 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();
    }
}

它调用了绘制所有元素并将像素复制到BitmapFramerenderScreen方法。 但是这个过程需要太长时间,所以我的 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# 的并发队列的信息,但这不是我需要的。 并且使用两个线程会导致编译器说该变量由另一个线程拥有的问题。

如何同时渲染新的 bitmap 并将 bitmap 每秒写入 LCD 60 次?

我假设USBD480_DrawFullScreenBGRA32是实际写入 LCD 的内容,而代码的 rest 只是准备图像。 我认为您提高性能的关键是在编写前一个图像准备下一个图像。

我认为您最好的解决方案是使用两个线程并使用ConcurrentQueue作为需要写入的缓冲区。 一个线程准备图像并将它们放入ConcurrentQueue ,另一个线程将它们从队列中拉出并将它们写入 LCD。 这样您就没有每次调用Task.Run的开销。

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

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

  1. 一个不断创建bitmap的;
  2. 一个连续获取最新的 bitmap 并将其推送到 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();
    }
}

您可以考虑使用健壮、高性能且高度可配置的TPL 数据流库,这将允许您构建数据管道。 您会将原始数据发布到管道的第一个块中,并且数据将在从一个块流向下一个块时进行转换,然后最终在最后一个块处呈现。 所有块将并行工作。 在下面的示例中,有三个块,都配置了默认的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
}

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM