Параллелизм RandomAccessFile в Java

Я создаю объект RandomAccessFile для записи в файл (на SSD) несколькими потоками. Каждый поток пытается записать прямой байтовый буфер в определенную позицию в файле, и я гарантирую, что позиция, в которую записывает поток, не будет перекрываться с другим потоком:

file_.getChannel().write(buffer, position);

где file_ - это экземпляр RandomAccessFile, а buffer - прямой байтовый буфер.

Для объекта RandomAccessFile, поскольку я не использую fallocate для выделения файла, а длина файла меняется, будет ли при этом использоваться параллелизм нижележащего носителя?

Если это не так, есть ли смысл использовать указанную выше функцию без вызова fallocate при создании файла?


person user1715122    schedule 30.07.2017    source источник
comment
Связанный вопрос   -  person Hovercraft Full Of Eels    schedule 30.07.2017
comment
Этот вопрос отличается, поскольку я использую интерфейс getChannel для записи в определенной позиции, а не для изменения текущей позиции файла.   -  person user1715122    schedule 30.07.2017
comment
Да, они достаточно разные, поэтому я не использовал могущественного Мьёльнира, чтобы закрыть этот вопрос, но в другом вопросе и его ответе есть кусочки, которые здесь актуальны.   -  person Hovercraft Full Of Eels    schedule 30.07.2017
comment
Из документации FileChannel, ваш вопрос отвечает сам, это зависит от реализации ОС: файловые каналы безопасны для использования несколькими параллельными потоками. [...] Другие операции, в частности те, которые занимают явную позицию, могут выполняться одновременно; действительно ли они это делают, зависит от базовой реализации и поэтому не определено.   -  person Adonis    schedule 03.08.2017


Ответы (2)


Я провел небольшое тестирование со следующим кодом:

   public class App {
    public static CountDownLatch latch;

    public static void main(String[] args) throws InterruptedException, IOException {
        File f = new File("test.txt");
        RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
        latch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(new WritingThread(i, (long) i * 10, file.getChannel()));
            t.start();

        }
        latch.await();
        file.close();
        InputStream fileR = new FileInputStream("test.txt");
        byte[] bytes = IOUtils.toByteArray(fileR);
        for (int i = 0; i < bytes.length; i++) {
            System.out.println(bytes[i]);

        }  
    }

    public static class WritingThread implements Runnable {
        private long startPosition = 0;
        private FileChannel channel;
        private int id;

        public WritingThread(int id, long startPosition, FileChannel channel) {
            super();
            this.startPosition = startPosition;
            this.channel = channel;
            this.id = id;

        }

        private ByteBuffer generateStaticBytes() {
            ByteBuffer buf = ByteBuffer.allocate(10);
            byte[] b = new byte[10];
            for (int i = 0; i < 10; i++) {
                b[i] = (byte) (this.id * 10 + i);

            }
            buf.put(b);
            buf.flip();
            return buf;

        }

        @Override
        public void run() {
            Random r = new Random();
            while (r.nextInt(100) != 50) {
                try {
                    System.out.println("Thread  " + id + " is Writing");
                    this.channel.write(this.generateStaticBytes(), this.startPosition);
                    this.startPosition += 10;
                } catch (IOException e) {
                    e.printStackTrace();

                }
            }
            latch.countDown();
        }
    }
}

Пока что я видел:

  • Windows 7 (раздел NTFS): запускается линейно (также известный как один поток пишет, а когда он завершается, запускается другой)

  • Linux Parrot 4.8.15 (раздел ext4) (дистрибутив на основе Debian) с ядром Linux 4.8.0: потоки перемешиваются во время выполнения

Опять же, как сказано в документации:

Файловые каналы безопасны для использования несколькими параллельными потоками. Метод закрытия может быть вызван в любое время, как указано в интерфейсе канала. В любой момент времени может выполняться только одна операция, которая затрагивает позицию канала или может изменить размер его файла; попытки инициировать вторую такую ​​операцию во время выполнения первой будут блокироваться до завершения первой операции. Другие операции, в частности те, которые занимают явную позицию, могут выполняться одновременно; действительно ли они это делают, зависит от базовой реализации и поэтому не уточняется.

Поэтому я предлагаю сначала попробовать и посмотреть, поддерживает ли ОС, в которых вы собираетесь развернуть свой код (возможно, тип файловой системы) параллельное выполнение вызова FileChannel.write.

Изменить. Как уже указывалось выше, это не означает, что потоки могут одновременно записывать в файл, это на самом деле противоположное, поскольку вызов write ведет себя в соответствии с контрактом WritableByteChannel, который четко указывает, что только один поток может писать в данный файл:

Если один поток инициирует операцию записи в канале, то любой другой поток, который пытается инициировать другую операцию записи, будет заблокирован до завершения первой операции.

person Adonis    schedule 03.08.2017
comment
Да, но мой вопрос в том, что, поскольку запись в определенную позицию всегда будет изменять длину файла (если только она не предшествует текущему eof файла), не все ли такие операции всегда будут сериализованы (независимо от базовой реализации)? - person user1715122; 05.08.2017
comment
@ user1715122 EOF не представляет большой проблемы, поскольку размер файла увеличивается для размещения буфера, превышающего размер файла. Если под сериализацией вы имеете в виду, что только один поток может писать в любой момент времени, в этом случае ответ - да, поскольку вызов write от FileChannel ведет себя как WritableByteChannel. - person Adonis; 05.08.2017

Как указано в документации, и Адонис уже упоминает об этом, запись может выполняться только одним потоком за раз. Вы не добьетесь увеличения производительности за счет параллелизма, более того, вам следует беспокоиться о производительности только в том случае, если это актуальная проблема, потому что одновременная запись на диск может фактически снизить вашу производительность (вероятно, в меньшей степени для твердотельных накопителей, чем для жестких дисков).

Базовый носитель в большинстве случаев (SSD, HDD, сеть) является однопоточным - на самом деле на аппаратном уровне нет такой вещи, как поток, потоки - это не что иное, как абстракция.

В вашем случае носитель - SSD. В то время как SSD внутренне может записывать данные в несколько модулей одновременно (они могут достигать уровня параллелизма, при котором записи могут быть такими же быстрыми и даже превосходить чтение), внутренние структуры данных сопоставления являются общим ресурсом и поэтому конкурируют, особенно при частых обновлениях, таких как параллельные пишет. Тем не менее, обновления этой структуры данных происходят довольно быстро, поэтому беспокоиться не о чем, если только это не станет проблемой.

Но помимо этого, это всего лишь внутренняя часть SSD. Снаружи вы общаетесь через интерфейс Serial ATA, то есть по одному байту за раз (фактически пакеты в структуре информации кадра, FIS). Поверх этого находится ОС / файловая система, которая снова имеет, вероятно, конкурирующую структуру данных и / или применяет собственные средства оптимизации, такие как кэширование с отложенной записью.

Кроме того, поскольку вы знаете, что такое ваш носитель, вы можете оптимизировать его специально для этого, а твердотельные накопители действительно работают быстро, когда один поток записывает большой кусок данных.

Таким образом, вместо использования нескольких потоков для записи вы можете создать большой буфер в памяти (возможно, рассмотреть файл с отображением в память) и одновременно записывать в этот буфер. Сама память не используется, если вы гарантируете, что каждый поток обращается к собственному адресному пространству буфера. Как только все потоки выполнены, вы записываете этот буфер на SSD (не требуется при использовании файла с отображением в память).

См. Также это хорошее резюме о разработке для SSD: Резюме - что каждый программист должен знать о твердотельных накопителях

Смысл выполнения предварительного выделения (или, точнее, file_.setLength(), который фактически отображается на ftruncate) заключается в том, что изменение размера файла может использовать дополнительные циклы, и вы, возможно, не захотите этого избежать. Но опять же, это может зависеть от ОС / файловой системы.

person Gerald Mücke    schedule 07.08.2017