【问题标题】:Read large file multithreaded多线程读取大文件
【发布时间】:2017-11-27 19:17:51
【问题描述】:

我正在实现一个应该接收大文本文件的类。我想将它分成块,每个块由一个不同的线程保存,该线程将计算这个块中每个字符的频率。我希望启动更多线程以获得更好的性能,但事实证明性能越来越差。这是我的代码:

public class Main {

    public static void main(String[] args) 
    throws IOException, InterruptedException, ExecutionException, ParseException  
    {

        // save the current run's start time
        long startTime = System.currentTimeMillis();

        // create options 
        Options options = new Options();
        options.addOption("t", true, "number of threads to be start");

        // variables to hold options 
        int numberOfThreads = 1;

        // parse options
        CommandLineParser parser = new DefaultParser();
        CommandLine cmd;
        cmd = parser.parse(options, args);
        String threadsNumber = cmd.getOptionValue("t");
        numberOfThreads = Integer.parseInt(threadsNumber);

        // read file
        RandomAccessFile raf = new RandomAccessFile(args[0], "r");
        MappedByteBuffer mbb 
            = raf.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, raf.length());

        ExecutorService pool = Executors.newFixedThreadPool(numberOfThreads);
        Set<Future<int[]>> set = new HashSet<Future<int[]>>();

        long chunkSize = raf.length() / numberOfThreads;
        byte[] buffer = new byte[(int) chunkSize];

        while(mbb.hasRemaining())
        {
            int remaining = buffer.length;
            if(mbb.remaining() < remaining)
            {
                remaining = mbb.remaining();
            }
            mbb.get(buffer, 0, remaining);
            String content = new String(buffer, "ISO-8859-1");
            @SuppressWarnings("unchecked")
            Callable<int[]> callable = new FrequenciesCounter(content);
            Future<int[]> future = pool.submit(callable);
            set.add(future);

        }

        raf.close();

        // let`s assume we will use extended ASCII characters only
        int alphabet = 256;

        // hold how many times each character is contained in the input file
        int[] frequencies = new int[alphabet];

        // sum the frequencies from each thread
        for(Future<int[]> future: set)
        {
            for(int i = 0; i < alphabet; i++)
            {
                frequencies[i] += future.get()[i];
            }
        }
    }

}

//help class for multithreaded frequencies` counting
class FrequenciesCounter implements Callable
{
    private int[] frequencies = new int[256];
    private char[] content;

    public FrequenciesCounter(String input)
    {
        content = input.toCharArray();
    }

    public int[] call()
    {
        System.out.println("Thread " + Thread.currentThread().getName() + "start");

        for(int i = 0; i < content.length; i++)
        {
            frequencies[(int)content[i]]++;
        }

        System.out.println("Thread " + Thread.currentThread().getName() + "finished");

        return frequencies;
    }
}

【问题讨论】:

  • 您的硬件每秒只能从磁盘传输这么多字节。您要求阅读多少并不重要。
  • 磁盘不是多线程的。你的期望是错误的。
  • 那么,如果我将每个块保存为不同的文件,然后将每个文件传递给线程,它会变得更好吗?
  • @barni 没有。你仍然会有一个磁盘。这可能会让事情变得更糟。
  • 所以有点理论:如果你的代码本身除了等待网络或硬盘之类的开销之外没有瓶颈,那么它被称为I/O绑定。在这一点上,让它运行得更快的唯一方法是改进连接到机器本身的硬件。或者开始水平扩展以利用更多独立的机器。

标签: java multithreading future callable mappedbytebuffer


【解决方案1】:

正如 cmets 中所建议的,当从多个线程读取时,您(通常)不会获得更好的性能。相反,您应该处理您在多个线程上读取的块。通常处理会执行一些阻塞、I/O 操作(保存到另一个文件?保存到数据库?HTTP 调用?),如果你在多个线程上处理,你的性能会更好。

对于处理,您可能有 ExecutorService(具有合理数量的线程)。使用java.util.concurrent.Executors获取java.util.concurrent.ExecutorService的实例

拥有ExecutorService 实例,您可以submit 处理您的块。提交块不会阻塞。 ExecutorService 将开始在单独的线程中处理每个块(细节取决于 ExecutorService 的配置)。您可以提交RunnableCallable 的实例。

最后,在您提交所有项目后,您应该在您的 ExecutorService 中调用 awaitTermination。它将等到所有提交项目的处理完成。在 awaitTermination 返回后,您应该调用 shutdownNow() 以中止处理(否则它可能会无限期挂起,处理一些流氓任务)。

【讨论】:

  • 如果单个处理线程可以跟上读取速度,那么多线程是毫无意义的复杂化。
  • 他已经在一个线程中读取并在多个线程中处理,并且他已经在使用ExecutorService。这似乎没有回答这个问题。
【解决方案2】:

您的程序几乎肯定会受到从磁盘读取速度的限制。使用多线程对此没有帮助,因为该限制是对从磁盘传输信息的速度的硬件限制。

此外,同时使用 RandomAccessFile 和后续缓冲区可能会导致小幅减速,因为您是在读取数据之后但在处理之前移动内存中的数据,而不是仅仅在原地处理它。最好不要使用中间缓冲区。

通过从文件直接读取到最终缓冲区并在这些缓冲区被填充时分派这些缓冲区以供线程处理,而不是在处理之前等待整个文件被读取,您可能会稍微加快速度。但是,大部分时间仍会用于磁盘读取,因此任何加速都可能是微乎其微的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多