Table of Contents
本文介绍如何设计一个先入先出的 Stream。
1 背景
很多时候我们既要用程序获取数据,同时又要对数据进行及时地处理,FIFO 就是处理这种应用的最佳内存管理方式。
例如从网卡获取数据,同时将这些数据通过声卡播放/通过显示屏显示。获取数据的时候将数据写入内存,播放或显示过后将内存释放。 比较通用的方法是使用两个不同的线程,一个用于接收数据,一个用于处理数据。
2 实现
难点主要是多线程编程。如上文所述,两个线程中需要共享一些资源,最基本的共享包括:
- 存储数据的内存
- 数据的最新写入位置
- 数据的最新读取位置
读写位置的记录是为了协调好读写的顺序,避免出现“读”超越了“写”,如果始终使用的是一块固定的内存区域(即循环读写),那么也可能出现“写”超越“读”的情况。下面讨论实现 FIFO 的两种典型的内存分配方式。
2.1 循环读写
之所以循环就是因为读写操作始终在一块固定的内存区域上进行。程序在一开始就申请这样一块区域(C#中可用字节数组 byte[]),专门用于读写操作。当区域写满后,就返回开头,从头开始写;当读到结尾处,就返回开头,从头开始读。分别用读下标 Rpos、写下标 Wpos 来记录读和写的位置。
- 图中给出了这块内存区域和读写下标的初始状态:
可以看出初始化要完成三件事:
- New 一块 BufferSize 大小的内存区域;
- 初始化读下标位置 Rpos=0;
- 初始化写下标位置 Wpos=0;
- 写线程开始接收数据并写入内存区域:
此时读线程尚未启动。很多时候读的速度是匀速的(如播放语音、动态绘图),而写线程由于数据是从网络等处获得,速度会收到网络等资源性能影响。读线程滞后启动也是一种防止“读”在某些时刻超过“写”的方式。
- 读线程启动。读“尾随” 写在内存区域中移动:
读位置指示的是下一刻将要读到的内存位置;写位置指示的是下一刻将要写入的内存位置。 读和写之间内存区域的大小是不确定的,因为读和写其实都不是严格匀速(理由同上)。
- 循环
图中给出了写位置循环而读位置未循环的情况。写操作将已经读过的内存区域覆盖了。但是如果控制不好,也会出现“写”超越“读”的情况,申请一个较大内存区域可以在一定程度上避免这种现象的出现。
2.2 Block 流
- 写 需要内存时就申请一块(Block)内存(一块就是一个 byte[],用 List 结构将这些 Block 串在一起), 当写超过一个 Block 大小(或者第一次写时),就新申请一个 Block。
- 读 当读(假设只向前读,而不会向后读)完一个 Block 时,就将整个 Block 从 List 中去掉; 为了提高效率,Block 从 List 中去掉并不是将其删除,而是先放到一个 Stack 中。下次要申请新 Block 是就从 Stack 中 Pop 出来,这样做减少了对内存的频繁申请和释放。
- 使用锁机制防止读写同时操作内存
为了防止读写相互影响,必须使用锁,这是因为:
- 在读阶段,判断读是否将会超过写时,需要知道写的位置,如果写一直在继续,那么读线程就得不到正确的写位置。
- 同理在写解读,判读是否将覆盖读时,也会用的读的位置。
- 如果写暂停了,数据暂停到达 只要接收流没有停止,就会一直等待继续写。读也会一直继续。
- 写不会超过读 因为如果读的过快,就会申请新的 Block。但是如果一直不读,岂不是一直新申请?在 sdrsharp 的最初版代码中确实是这样的。要看 FiFoStream 用在什么地方,有没有可能只写不读呢?
Date:
Created: 2016-10-24 一 11:23