是否有关于如何以一定速度读取长文件的文章/算法?
假设我不想在发出读取时通过 10 KB/秒。
一个简单的解决方案,通过创建一个 ThrottledInputStream。
这应该像这样使用:
final InputStream slowIS = new ThrottledInputStream(new BufferedInputStream(new FileInputStream("c:\\file.txt"),8000),300);
300 是每秒千字节数。8000 是 BufferedInputStream 的块大小。
这当然应该通过实现 read(byte b[], int off, int len) 来概括,这将为您节省大量 System.currentTimeMillis() 调用。System.currentTimeMillis() 为每个字节读取调用一次,这可能会导致一些开销。还应该可以存储无需调用 System.currentTimeMillis() 即可轻松读取的字节数。
确保在两者之间放置一个 BufferedInputStream,否则 FileInputStream 将以单个字节而不是块进行轮询。这会将CPU负载从 10% 减少到几乎为 0。您将面临超出数据速率的风险,即块大小中的字节数。
import java.io.InputStream;
import java.io.IOException;
public class ThrottledInputStream extends InputStream {
private final InputStream rawStream;
private long totalBytesRead;
private long startTimeMillis;
private static final int BYTES_PER_KILOBYTE = 1024;
private static final int MILLIS_PER_SECOND = 1000;
private final int ratePerMillis;
public ThrottledInputStream(InputStream rawStream, int kBytesPersecond) {
this.rawStream = rawStream;
ratePerMillis = kBytesPersecond * BYTES_PER_KILOBYTE / MILLIS_PER_SECOND;
}
@Override
public int read() throws IOException {
if (startTimeMillis == 0) {
startTimeMillis = System.currentTimeMillis();
}
long now = System.currentTimeMillis();
long interval = now - startTimeMillis;
//see if we are too fast..
if (interval * ratePerMillis < totalBytesRead + 1) { //+1 because we are reading 1 byte
try {
final long sleepTime = ratePerMillis / (totalBytesRead + 1) - interval; // will most likely only be relevant on the first few passes
Thread.sleep(Math.max(1, sleepTime));
} catch (InterruptedException e) {//never realized what that is good for :)
}
}
totalBytesRead += 1;
return rawStream.read();
}
}
粗略的解决方案是一次读取一个块,然后休眠,例如 10k,然后再休眠一秒钟。但我要问的第一个问题是:为什么?有几个可能的答案:
我的建议是不要在读取级别控制它。这有点混乱和不准确。而是在工作结束时控制它。Java 有很多很棒的并发工具来处理这个问题。有几种替代方法可以做到这一点。
我倾向于使用生产者消费者模式来解决这类问题。它为您提供了很好的选择,可以通过报告线程等来监控进度,它可以是一个非常干净的解决方案。
像ArrayBlockingQueue这样的东西可用于 (1) 和 (2) 所需的那种节流。由于容量有限,当队列已满时,阅读器最终会阻塞,因此不会很快填满。可以控制工人(消费者)只工作如此之快,以限制速率覆盖(2)。
按照建议创建采用另一个 InputStream 的 ThrottledInputStream 将是一个不错的解决方案。
如果您使用过 Java I/O,那么您应该熟悉装饰流。我建议使用InputStream
另一个子类InputStream
并限制流速。(您可以进行子类FileInputStream
化,但这种方法极易出错且不灵活。)
您的具体实施将取决于您的具体要求。通常,您需要注意最后一次读取返回的时间 ( System.nanoTime
)。在当前读取中,在底层读取之后,wait
直到足够的时间用于传输的数据量。更复杂的实现可能会缓冲并(几乎)立即返回(几乎)只有速率指示的数据(请注意,如果缓冲区长度为零,则只应返回读取长度为 0)。
您可以使用 RateLimiter。并在 InputStream 中自己实现读取。这方面的一个例子可以在下面看到
public class InputStreamFlow extends InputStream {
private final InputStream inputStream;
private final RateLimiter maxBytesPerSecond;
public InputStreamFlow(InputStream inputStream, RateLimiter limiter) {
this.inputStream = inputStream;
this.maxBytesPerSecond = limiter;
}
@Override
public int read() throws IOException {
maxBytesPerSecond.acquire(1);
return (inputStream.read());
}
@Override
public int read(byte[] b) throws IOException {
maxBytesPerSecond.acquire(b.length);
return (inputStream.read(b));
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
maxBytesPerSecond.acquire(len);
return (inputStream.read(b,off, len));
}
}
如果您想将流量限制为 1 MB/s,您可以像这样获得输入流:
final RateLimiter limiter = RateLimiter.create(RateLimiter.ONE_MB);
final InputStreamFlow inputStreamFlow = new InputStreamFlow(originalInputStream, limiter);
这在一定程度上取决于您的意思是“不超过某个比率”还是“接近某个比率”。
如果您的意思是“不超过”,您可以通过一个简单的循环来保证:
while not EOF do
read a buffer
Thread.wait(time)
write the buffer
od
等待的时间是缓冲区大小的一个简单函数;如果缓冲区大小为 10K 字节,则您希望在读取之间等待一秒钟。
如果你想比这更近,你可能需要使用计时器。
如果您担心将数据传递给其他东西的速度,而不是控制读取,将数据放入队列或循环缓冲区等数据结构中,并控制另一端;定期发送数据。但是,您需要注意这一点,具体取决于数据集大小等,因为如果读取器比写入器快得多,您可能会遇到内存限制。