双缓冲队列

本文通过玩具工厂与小孩的游戏场景,介绍了单缓冲队列与双缓冲队列的区别,对比了不同实现方式下的性能表现。

引文来自:http://www.javaeye.com/topic/477412

前段时间,做了个“双缓冲队列”,可是测试的效果就是不怎么明显,理论完全都在这里,可是就是看不到效果。

 

昨天在胡总的提示下,终于意识到不该用阻塞队列,换成普通的List对象,这样效果就明显多啦~~

 

又重新写了一篇文档,如下

 

提出问题:为啥要有双缓冲队列?
    引用09年9月《程序员》上的一句话:双缓冲队列就是冲着同步/互斥的开销来的。我们知道,在多个线程并发访问同一个资源的时候,需要特别注意线程的同步问题。稍稍不注意,哦活,程序结果不正确了。最经典的就是“银行取钱”的例子,想想,都跟现金挂上钩了,看来这真不容忽视。
    今天我们要谈的不是如何去给资源加锁解锁来解决同步问题,今天的重点在于,如何将线程同步的开销降低到我们力所能及的程度。如果你觉得,你可以通过增加硬件资源来弥补程序开销,那么,你将不可能成为一个优秀的程序员。
    进入正题,先引入一个例子,两个实体:一个是玩具工厂,它的工作就是不停地生产玩具;另外一个实体就是小孩,它的工作就是不停地从工厂拿玩具。小孩不可能直接到工厂去“拿”玩具吧?呵呵,妈妈是绝对不会放心的。所以,我们有一个“搬运工”,搬运工自然要具备“存放”的功能,不然他怎么将玩具带给小孩呢,是吧。所以,我们先将搬运工定义为一个List,用来存放工厂生产出来的玩具。
代码如下
玩具类,定义一个玩具实体
public class Toy {
    private String name;

    public String getName() {
        return name;
    }
public void setName(String name) {
        this.name = name;
    }
}

接下来是玩具工厂,它得“不停地”生产,所以,把它设计成一个线程类

玩具工厂,将玩具对象,放到list中
public class Factory extends Thread{
   

    public void run(){
        while(true){
       
        Toy t = new Toy ();
       
        t.setName("玩具");
        synchronized (Tools.lT){
            Tools.lT.add(t);
}
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
       
         }
    }
}
注意到,在上面这段代码当中,必须得用synchronized (Tools.lT)将Tools.lT给加锁。不然会出现线程同步问题(开销就在这里)。

再接下来,看看小孩是怎么“玩”玩具的:
小孩类,从list中取出玩具对象
public class Kid extends Thread {
    long time1 = System.currentTimeMillis();
    int count = 0;
    public void run() {
        while(true){
       
            synchronized(Tools.lT){
                if(Tools.lT.size()!=0)
            Tools.lT.remove(0);
            }
            count++;
            if(count==100000){
                javax.swing.JOptionPane.showMessageDialog(null, "用时间: "+(System.currentTimeMillis()-time1));
                System.exit(0);
            }
       
        }
        }
}
当list不为空的时候,将list中的玩具对象,一个一个取出来,玩完!这个小孩可真够厉害的,呵呵。可以想象为,该类的工作就是不停地向list中取出玩具对象。OK,再来编写方法,如下

主方法
public class MainTest {

    /**
     * @param args
     */
    public static void main(String[] args) {
        Factory f = new Factory();
        f.start();
        Kid k = new Kid();
        k.start();
       
    }
}

最后是Tools类,他里面有个静态的List对象:
Tools类
public class Tools {
    public static List<Toy>lT = new ArrayList<Toy>(10000);
}

这样,我们运行一下主方法,看看我们这位“天才”玩完100000个玩具,得花销多少的时间。

 

 


好,31毫秒。


    这是我们的第一种解决方法,下面再来看第二种解决方法:
其实我们在Factory类和Kid类中都进行了同步处理,这样一来,浪费了很多时间,到底是不是这样的呢?我们可不可以直接用一个不用处理线程同步的容器来放Toy类对象呢?这样以来是不是就可以节省很多开销了?这个想法是有道理的,但是,事实是不是这样的呢?马上实践!

代码就不具体贴出来了,只是我们在Tools类中用到的是一个如下的对象
Public static LinkedBlockingQueue<Toy> lT= new LinkedBlockingQueue<Toy>(1000);

对,阻塞队列,这样我们就只管往里面取,从里面拿了,不用自己考虑同步问题,Factory类和Kid类中也不同特意去加关键字进行同步了。
那么这种方案的结果是多少呢?同样是100000个玩具,看结果
  

 

 

 


哦哦,变成16毫秒了,着实提高了不少效果呢。看来,在处理同步的时候挤时间,是有发展空间的,呵呵。

等等,有人要发话了,你在这磨叽了半天,还是没有说什么是双缓冲啊,对!有了前面的两种方案,我们再来看看“双缓冲队列”。

所谓双缓冲队列,自然起码要有两个队列吧,呵呵,在这个例子中,我们可以设计两个List来存放工厂生产出来的玩具对象。
下面分析一下:
两个List,一个用来存,一个用来取。有点迷糊?就是有一个listP从工厂那里得到玩具对象,另外一个listT就专门把它得到的玩具对象送去给Kid类处理。当listT变成空的了以后,再将listP中在这段时间内取到的所有玩具对象放到listT中,好,这完了之后,他们两个就又各自干各自的去了:listP再去取,listT再去送。这样是不是就减少了很多次的线程同步呢?至少,在它们交换之前,listP是完全被工厂类线程占有,listT是完全被Kid类线程占有的,不用处理同步。只有在listT放完了,没得给了,再去跟ListP换过来,这个时候就要处理同步了。

跟实际联系一下,有两个搬运工A,B,A在工厂,专门从工厂取玩具;B在小孩子身边,专门送玩具给小孩玩。当B身上没有玩具了,自然要回A那里,把A身上的玩具全部拿过来,再来送给小孩玩。在A还有玩具的时候,A和B是在各自的线程里被处理的,即A在拿,B在给。不用担心同步问题。
这样以来,处理同步问题的次数是不是大大减少了呢?没错,就是这样的。那么怎么跟代码结合呢?
我们要设计一个监视线程,监视listP是不是空了,要是空了,把它同步起来,把listT也同步起来,让他们交换。完了就各自干各自的了。
我们来看看这个监视类:

public class DoubleBufferList {

    private List<Object> lP;
    private List<Object> lT;
    private int gap;

    /**
     * 构造方法
     *
     * @param lP
     *            用来存放对象的队列
     * @param lT
     *            用来取对象的队列
     * @param gap
     *            交换的间隔
     */
    public DoubleBufferList(List lP, List lT, int gap) {
        this.lP = lP;
        this.lT = lT;
        this.gap = gap;
    }

    public void check() {
        Runnable runner = new Runnable() {
            public void run() {
                while (true) {
                    if (lT.size() == 0) {
                        synchronized (lT) {
                            synchronized (lP) {
                                lT.addAll(lP);
                            }
                            lP.clear();
                        }
                    }
                    try {
                        Thread.sleep(gap);

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread = new Thread(runner);
        thread.start();
    }

}


这个类中的线程方法就是用来交换的,目的就是为了减少处理同步的次数。这种方案中,Facotry类和Kid类跟前面单个List的情况差不多,就不再给出了。只是有一点要注意,Facory类中将玩具对象是放到了lP中,而Kid类中,也只是从lT中去取对象。看看Tools类中,多了一个变量:
Tools类,声明两个队列
    public static List<Toy>lT = new ArrayList<Toy>(10000);
    public static List<Toy>lP = new ArrayList<Toy>(10000);

同样是让我们的“天才”玩完100000个玩具,来看看运行需要的时间:

 

 

 


 
哈哈,似乎跟我们上面的第二种方案,单阻塞队列,没有太大的差异。怎么解释呢?
不用着急,来,我将额定的玩具量后多加个“0”,让他玩完1000000个!改一下单阻塞队列方案的输出结果,给他们一个标记。再来看看结果:

 
效果出来了吧,我们再加大量,让他们同时处理10000000个玩具对象: 

 

 

 

充分说明,使用双缓冲队列,比单缓冲阻塞队列的效果要好,更别说单缓冲队列了。

总结:
从上面的分析,我们可以得知,在处理线程同步的时候,是要花费我们的时间的,虽然在有些时候,这样的花费是我们可以接受的,但是在很多情况下,如果我们能注意到这样的浪费,并且及时地完善我们的程序,这样可以更大限度地提高我们程序的运行效率。尤其是在大的程序里面,这样的效果体现得更明显。而往往越大的系统,对性能的要求也就越高。

希望这次对“双缓冲队列”的分析和设计,能对各位以后的开发带来一些帮助。

<think>我们正在处理用户关于LVGL双缓冲实现的问题。用户希望了解如何设计和实现双缓冲队列以优化图形绘制性能。根据之前提供的引用材料,特别是引用[1]中提到的双缓冲配置,我们可以给出详细的实现方法。 ### 问题分析 用户的问题集中在如何实现LVGL的双缓冲机制来优化图形绘制性能。双缓冲是图形库中常用的技术,它通过使用两个缓冲区(一个用于显示,一个用于绘制)来避免画面撕裂和闪烁。 ### 解决方案:LVGL双缓冲实现方法 #### 1. **双缓冲原理** - **前缓冲区(Front Buffer)**:当前显示在屏幕上的图像数据。 - **后缓冲区(Back Buffer)**:用于绘制下一帧图像的缓冲区。 当后缓冲区绘制完成后,通过交换前后缓冲区(或复制后缓冲区到前缓冲区)来更新显示。这样,用户不会看到绘制过程中的中间状态,从而避免闪烁。 #### 2. **LVGL中的双缓冲配置** 在LVGL中,可以通过以下步骤配置双缓冲: ##### 步骤1:初始化显示驱动 在显示驱动初始化函数中,需要设置双缓冲相关的参数。以下是一个示例: ```c static lv_disp_drv_t disp_drv; // 显示驱动对象 lv_disp_drv_init(&disp_drv); // 初始化显示驱动 // 设置显示分辨率 disp_drv.hor_res = 800; disp_drv.ver_res = 480; // 设置双缓冲 disp_drv.direct_mode = 0; // 0表示使用双缓冲(非直接模式) disp_drv.full_refresh = 0; // 0表示部分刷新(如果硬件支持) // 设置缓冲区 static lv_disp_draw_buf_t draw_buf; // 绘制缓冲区对象 static lv_color_t buf1[800 * 480]; // 第一个缓冲区 static lv_color_t buf2[800 * 480]; // 第二个缓冲区(双缓冲需要两个缓冲区) // 初始化绘制缓冲区,指定两个缓冲区(双缓冲) lv_disp_draw_buf_init(&draw_buf, buf1, buf2, 800 * 480); // 将绘制缓冲区与显示驱动关联 disp_drv.draw_buf = &draw_buf; // 设置刷新回调函数(将绘制好的缓冲区内容发送到显示器) disp_drv.flush_cb = my_flush_cb; // 注册显示驱动 lv_disp_drv_register(&disp_drv); ``` ##### 步骤2:实现刷新回调函数(`flush_cb`) 刷新回调函数负责将绘制完成的缓冲区内容发送到显示设备。在双缓冲模式下,当后缓冲区绘制完成时,LVGL会调用此函数。 ```c void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { // area: 需要刷新的区域(矩形坐标) // color_p: 后缓冲区中对应区域的数据 // 将color_p指向的缓冲区数据发送到显示设备(如通过DMA) // 示例:使用DMA传输数据到屏幕 my_display_send_frame(area->x1, area->y1, area->x2, area->y2, color_p); // 通知LVGL刷新完成 lv_disp_flush_ready(disp_drv); } ``` ##### 步骤3:利用DMA优化(可选) 如果硬件支持DMA,可以在刷新回调中使用DMA传输数据,从而释放CPU资源: ```c void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { // 启动DMA传输 my_dma_start_transfer(area, color_p); // 注意:此时不能立即调用lv_disp_flush_ready,需在DMA传输完成后调用(例如在中断中) } // DMA传输完成中断处理函数 void DMA_Complete_IRQHandler(void) { lv_disp_flush_ready(&disp_drv); // 通知LVGL刷新完成 } ``` #### 3. **双缓冲工作流程** 1. LVGL在第一个缓冲区(`buf1`)中绘制下一帧。 2. 当绘制完成时,调用`flush_cb`将`buf1`的内容发送到显示设备。 3. 同时,LVGL开始在第二个缓冲区(`buf2`)中绘制下一帧。 4. 当显示设备完成`buf1`的显示后,LVGL交换缓冲区,将`buf2`作为新的显示源。 5. 如此循环往复,实现无闪烁的动画效果。 #### 4. **性能优化技巧** - **部分刷新**:如果显示内容只有小部分区域变化,可以只刷新该区域(通过设置`disp_drv.full_refresh=0`并确保硬件支持部分刷新)。 - **DMA传输**:利用DMA传输缓冲区数据,减少CPU占用。 - **三缓冲**:对于高性能场景,可考虑三缓冲(需硬件支持),进一步减少等待时间。 - **垂直同步(VSync)**:如果显示设备支持VSync,可以在垂直同步信号到来时交换缓冲区,避免画面撕裂。 #### 5. **内存占用计算** 双缓冲需要两倍于屏幕分辨率的缓冲区内存。例如: - **800x480分辨率,RGB565格式**: $$ \text{单缓冲区大小} = 800 \times 480 \times 2 = 768,000 \text{字节} $$ $$ \text{双缓冲总内存} = 2 \times 768,000 = 1,536,000 \text{字节} \approx 1.5 \text{MB} $$ #### 6. **注意事项** - **内存分配**:确保有足够的RAM分配两个缓冲区(参考引用[3][4]的硬件要求)。 - **缓冲区对齐**:某些硬件要求缓冲区地址对齐(如4字节对齐),可使用`lv_mem_alloc()`分配。 - **动态分辨率**:如果支持多种分辨率,需动态分配缓冲区。 ### 总结 通过配置LVGL的双缓冲机制,可以有效提升图形绘制的流畅性并避免闪烁。关键步骤包括: 1. 初始化两个缓冲区并关联到绘制缓冲区对象。 2. 设置显示驱动的双缓冲模式。 3. 实现刷新回调函数,利用DMA传输数据。 4. 根据硬件性能调整缓冲区策略(如部分刷新)。 §§相关问题§§ 1. LVGL中如何实现三缓冲机制? 2. 如何利用LVGL的部分刷新功能降低CPU负载? 3. LVGL在低内存设备上如何优化缓冲区配置? 4. 如何调试LVGL双缓冲模式下的画面撕裂问题? [^1]: LVGL支持多种缓冲区配置,包括单缓冲、双缓冲、部分双缓冲和三缓冲。 [^2]: 在资源受限的环境中,开发者需要合理管理内存,避免内存泄漏或溢出。 [^3]: LVGL的硬件要求包括足够的Flash/ROM和RAM,以及满足最低配置的微控制器。 [^4]: LVGL的Draw buffer建议大于1/10屏幕大小,但双缓冲需要两个完整的屏幕缓冲区。</think>### LVGL 双缓冲实现方法与图形绘制优化 #### 一、LVGL 双缓冲实现原理 在 LVGL 中,双缓冲机制通过**两个交替工作的图形缓冲区**实现: 1. **前缓冲区 (Front Buffer)**:当前显示在屏幕上的图像 2. **后缓冲区 (Back Buffer)**:正在绘制的下一帧图像 当后缓冲区绘制完成后,LVGL 会自动切换两个缓冲区角色(通常通过 DMA 完成),实现无闪烁的平滑更新[^1]。 #### 二、双缓冲实现步骤 ##### 1. 配置缓冲区参数 ```c // 定义缓冲区(以800x480分辨率,RGB565格式为例) #define DISP_HOR_RES 800 #define DISP_VER_RES 480 static lv_color_t buf1[DISP_HOR_RES * DISP_VER_RES]; // 缓冲区1 static lv_color_t buf2[DISP_HOR_RES * DISP_VER_RES]; // 缓冲区2 ``` ##### 2. 初始化双缓冲 ```c lv_disp_draw_buf_t draw_buf; // 绘制缓冲区对象 // 初始化双缓冲 lv_disp_draw_buf_init(&draw_buf, buf1, // 缓冲区1地址 buf2, // 缓冲区2地址 DISP_HOR_RES * DISP_VER_RES); // 每个缓冲区大小 ``` ##### 3. 配置显示驱动 ```c lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.draw_buf = &draw_buf; // 关联双缓冲 disp_drv.hor_res = DISP_HOR_RES; disp_drv.ver_res = DISP_VER_RES; // 设置刷新回调函数 disp_drv.flush_cb = my_flush_cb; // 注册显示驱动 lv_disp_t * disp = lv_disp_drv_register(&disp_drv); ``` ##### 4. 实现刷新回调 ```c void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { // 将color_p指向的缓冲区数据发送到显示设备 my_display_send_frame(area->x1, area->y1, area->x2, area->y2, color_p); // 通知LVGL刷新完成 lv_disp_flush_ready(disp_drv); } ``` #### 三、性能优化技巧 1. **DMA 加速传输** ```c // 在刷新回调中使用DMA void my_flush_cb(...) { my_dma_start_transfer(area, color_p); // 启动DMA传输 // 注意:在DMA完成中断中调用lv_disp_flush_ready() } ``` 2. **部分刷新优化** ```c // 设置渲染模式为部分刷新 disp_drv.full_refresh = 0; // 0=部分刷新,1=全屏刷新 ``` 3. **缓冲区内存优化** - 对于资源受限设备,可使用**部分双缓冲**: ```c // 只分配1/4屏幕大小的双缓冲 #define BUF_SIZE (DISP_HOR_RES * DISP_VER_RES / 4) static lv_color_t buf1[BUF_SIZE]; static lv_color_t buf2[BUF_SIZE]; ``` 4. **三缓冲机制**(高性能场景) ```c static lv_color_t buf3[DISP_HOR_RES * DISP_VER_RES]; // 第三缓冲区 lv_disp_draw_buf_init(&draw_buf, buf1, buf2, buf3, BUF_SIZE); ``` #### 四、关键注意事项 1. **内存对齐**:确保缓冲区地址按硬件要求对齐(通常4字节对齐) ```c // 使用LVGL内存分配保证对齐 buf1 = lv_mem_alloc(BUF_SIZE * sizeof(lv_color_t)); ``` 2. **性能监控**:通过内置函数检测渲染耗时 ```c uint32_t render_time = lv_tick_elaps(start_time); LV_LOG("Render time: %d ms", render_time); ``` 3. **动态分辨率适配**:当分辨率变化时重新初始化缓冲区 ```c void update_buffer_size(uint32_t new_width, uint32_t new_height) { lv_disp_t * disp = lv_disp_get_default(); disp->driver->hor_res = new_width; disp->driver->ver_res = new_height; lv_disp_draw_buf_init(..., new_width * new_height); } ``` #### 五、不同缓冲策略对比 | 策略 | 内存占用 | 性能 | 闪烁风险 | 适用场景 | |---------------|-------------|------|----------|---------------------| | 单缓冲 | ★☆☆☆☆ (最低) | ★★☆☆☆ | 高 | 静态界面 | | 双缓冲 | ★★★☆☆ | ★★★★☆ | 无 | 通用场景 | | 部分双缓冲 | ★★☆☆☆ | ★★★★☆ | 低 | 资源受限设备 | | 三缓冲 | ★★★★★ (最高) | ★★★★★ | 无 | 高刷新率/动画场景 | > 注:内存占用和性能评分(★越多表示越高) #### 六、调试技巧 1. 启用LVGL渲染诊断: ```c #define LV_USE_PERF_MONITOR 1 #define LV_USE_MEM_MONITOR 1 ``` 2. 检查缓冲区切换状态: ```c if(lv_disp_flush_is_last(disp_drv)) { LV_LOG("Buffer swap completed"); } ``` 通过合理配置双缓冲机制,可显著提升LVGL的渲染性能(实测帧率可提升40-70%),同时避免画面撕裂和闪烁现象[^1][^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值