【问题标题】:How to process contents from large text file using multiple threads?如何使用多个线程处理大型文本文件中的内容?
【发布时间】:2017-06-29 08:58:55
【问题描述】:

我必须阅读一个包含文本的大文件,大约 3GB(和 4000 万行)。只是阅读它发生得非常快:

try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
  while ((line = br.readLine()) != null) {
    //Nothing here
  }
}

每次从上面的代码中读取line,我都会对字符串进行一些解析并进一步处理它。(一项艰巨的任务)。我尝试这样做多线程

A)我试过BlockingQueue这样

try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
            String line;
            BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
            int numThreads = 5;
            Consumer[] consumer = new Consumer[numThreads];
            for (int i = 0; i < consumer.length; i++) {
                consumer[i] = new Consumer(queue);
                consumer[i].start();
            }
            while ((line = br.readLine()) != null) {
                queue.put(line);
            }
            queue.put("exit");
        } catch (FileNotFoundException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException | InterruptedException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        }

class Consumer extends Thread {
        private final BlockingQueue<String> queue;
        Consumer(BlockingQueue q) {
            queue = q;
        }
        public void run() {
            while (true) {
                try {
                    String result = queue.take();
                    if (result.equals("exit")) {
                        queue.put("exit");
                        break;
                    }
                    System.out.println(result);
                } catch (InterruptedException ex) {
                    Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
                }
            }

        }
    }

这种方法比普通的单线程处理花费更多的时间。 我不知道为什么 - 我做错了什么?

B)我试过ExecutorService

 try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
            String line; 
            ExecutorService pool = Executors.newFixedThreadPool(10);         
             while ((line = br.readLine()) != null) {
                 pool.execute(getRunnable(line));
            }
             pool.shutdown();
         } catch (FileNotFoundException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        }

  private static Runnable getRunnable(String run){
        Runnable task = () -> {
            System.out.println(run);
        };
        return task;
    }

这种方法也比直接在 while 循环内打印需要更多时间。我做错了什么?

正确的做法是什么?

如何高效处理多线程读取line

【问题讨论】:

  • 这只是我的猜测:您的瓶颈是阅读本身,而不是处理。所以处理已经在不同线程上读取的行不会加速。快速搜索给了我这个结果:stackoverflow.com/questions/25711616/… 和接受的回复:stackoverflow.com/questions/9093888/…
  • 您的第二个建议似乎很地道。我建议并行化更大的数据块而不是单行,例如,每个块 1 MB。
  • 您的示例并不适合测试多线程。例如,在情况 A 中,您的所有线程都被“队列”阻塞,然后是系统 i/o。尝试一些真实的代码,可能会看到不同。
  • 您说读取数据的解析将是一项“巨大的任务”。只需在您的消费者和每次读取原始单线程代码中的行之后添加一些等待时间,然后检查多线程版本的性能是否更好。
  • 顺便说一句,使用System.out.println() 来测试多线程是一个非常糟糕的主意,因为它是同步的。因此,您的代码实际上将是单线程的,但会增加多线程的所有开销。

标签: java multithreading


【解决方案1】:

在这里回答一部分 - 为什么BlockingQueue 选项较慢。

了解线程并不是“免费”的,这一点很重要。总是需要一定的开销来让它们起来并“管理”它们。

当然,当您实际使用的线程数超过硬件“本机”支持的线程数时,上下文切换就会被添加到账单中。

除此之外,BlockingQueue 也不是免费的。你看,为了preserve order,ArrayBlockingQueue 可能必须在某处同步。最坏的情况,这意味着锁定和等待。是的,JVM 和 JIT 通常在这些方面做得很好;但同样,一定的“百分比”会被添加到账单中。

但这只是为了记录,这不重要。来自javadoc

此类支持对等待的生产者和消费者线程进行排序的可选公平策略。默认情况下,不保证此排序。但是,公平性设置为 true 的队列以 FIFO 顺序授予线程访问权限。公平通常会降低吞吐量,但会降低可变性并避免饥饿。

因为你没有设置“公平”

BlockingQueue queue = new ArrayBlockingQueue(100);

这不应该影响你。另一方面:我很确定您希望这些行按顺序处理,因此您实际上会想要

BlockingQueue<String> queue = new ArrayBlockingQueue<>(100, true);

从而进一步减慢整个过程。

最后:我同意到目前为止给出的 cmets。对这些事物进行基准测试是一项复杂的工作。很多方面都会影响结果。最重要的问题肯定是:你的瓶颈在哪里?!是 IO 性能(然后更多的线程没有多大帮助!) - 还是真的是整体处理时间(然后“正确”的处理线程数肯定会加快速度)。

关于“如何以正确的方式做到这一点” - 我建议在 softwareengineering.SE 上查看question

【讨论】:

  • 对这样的代码进行基准测试的一个很好的起点是简单地监控队列的大小。如果队列几乎总是空的,这意味着瓶颈在生产者方面(即你的 I/O 操作太慢了,你不会通过多线程处理部分赢得任何东西)。
【解决方案2】:

如何使用多线程处理大文本文件中的内容?

如果您的计算机有足够的 RAM,我会执行以下操作:

  • 将整个文件读入一个变量(例如 ArrayList) - 仅使用一个线程来读取整个文件。

  • 然后启动一个 ExecutorService(使用的线程池不超过您的计算机可以同时运行的最大内核数)

        int cores = Runtime.getRuntime().availableProcessors();
        ExecutorService executorService = Executors.newFixedThreadPool(cores);
    
  • 最后,将读取的行划分为有限数量的 callables/runnables,并将这些 callables/runnables 提交给您的 ExecutorService(这样它们都可以在您的 ExecutorService 中同时执行)。

除非您的行处理使用 I/O,否则我假设您将达到接近 100% 的 CPU 利用率,并且您的所有线程都不会处于等待状态。

您想要更快的处理速度吗?

垂直扩展是最简单的选择:购买更多 RAM、更好的 CPU(具有更多内核)、使用固态驱动器

【讨论】:

    【解决方案3】:

    可能是所有线程同时访问相同的共享资源,因此会产生更多的争议。 您可以尝试阅读器线程将所有行放在单个密钥中的一件事以分区方式提交,这样争议就会减少。

    public void execute(可运行命令) {

        final int key= command.getKey();
         //Some code to check if it is runing
        final int index = key != Integer.MIN_VALUE ? Math.abs(key) % size : 0;
        workers[index].execute(command);
    }
    

    创建带有队列的工作人员,以便如果您需要按顺序执行某些任务,然后运行。

    private final AtomicBoolean scheduled = new AtomicBoolean(false);
    
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(maximumQueueSize);
    
    public void execute(Runnable command) {
        long timeout = 0;
        TimeUnit timeUnit = TimeUnit.SECONDS;
        if (command instanceof TimeoutRunnable) {
            TimeoutRunnable timeoutRunnable = ((TimeoutRunnable) command);
            timeout = timeoutRunnable.getTimeout();
            timeUnit = timeoutRunnable.getTimeUnit();
        }
    
        boolean offered;
        try {
            if (timeout == 0) {
                offered = workQueue.offer(command);
            } else {
                offered = workQueue.offer(command, timeout, timeUnit);
            }
        } catch (InterruptedException e) {
            throw new RejectedExecutionException("Thread is interrupted while offering work");
        }
    
        if (!offered) {
            throw new RejectedExecutionException("Worker queue is full!");
        }
    
        schedule();
    }
    
    private void schedule() {
        //if it is already scheduled, we don't need to schedule it again.
        if (scheduled.get()) {
            return;
        }
    
        if (!workQueue.isEmpty() && scheduled.compareAndSet(false, true)) {
            try {
                executor.execute(this);
            } catch (RejectedExecutionException e) {
                scheduled.set(false);
                throw e;
            }
        }
    }
    
    public void run() {
        try {
            Runnable r;
            do {
                r = workQueue.poll();
                if (r != null) {
                    r.run();
                }
            }
            while (r != null);
        } finally {
            scheduled.set(false);
            schedule();
        }
    }
    

    如上所述,线程池大小没有固定规则。但是根据您的用例,可以使用一些建议或最佳实践。

    CPU 密集型任务

    For CPU bound tasks, Goetz (2002, 2006) recommends
    
    threads = number of CPUs + 1
    

    IO 绑定任务

    Working out the optimal number for IO bound tasks is less obvious. During an IO bound task, a CPU will be left idle (waiting or blocking). This idle time can be better used in initiating another IO bound request.
    

    【讨论】:

      猜你喜欢
      • 2016-09-02
      • 2020-10-01
      • 2021-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-11
      • 2019-06-29
      相关资源
      最近更新 更多