Запись в файл из нескольких потоков

Я пишу менеджер загрузки в Objective-C, который загружает файл из нескольких сегментов одновременно, чтобы улучшить скорость. Каждый сегмент файла загружается в поток.

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

Итак, я ищу способ записи в файл в определенной позиции и который способен обрабатывать несколько потоков, потому что в моем приложении, каждый сегмент загружается в отдельном потоке. В Java, я знаю, что FileChannel делает трюк отлично, но я понятия не имею в Objective-C.

3 ответов


никогда не забывайте, что Obj-C базируется на нормальном C, и поэтому я бы просто написал собственный класс, который обрабатывает файловый ввод/вывод с помощью стандартного API C, который позволяет размещать текущую позицию записи в любом месте в новом файле, даже далеко за пределами текущего размера файла (отсутствующие байты заполнены нулевыми байтами), а также прыгать вперед и назад, как вы хотите. Самый простой способ достичь потокобезопасности-использовать блокировку, это не самый быстрый способ, но в вашем конкретном случае, я уверен, что узкое место конечно, это не синхронизация потоков. У класса может быть такой заголовок:

@interface MultiThreadFileWriter : NSObject
{
    @private
        FILE * i_outputFile;
        NSLock * i_fileLock;
}
- (id)initWithOutputPath:(NSString *)aFilePath;
- (BOOL)writeBytes:(const void *)bytes ofLength:(size_t)length
    toFileOffset:(off_t)offset;
- (BOOL)writeData:(NSData *)data toFileOffset:(off_t)offset;
- (void)close;
@end

и реализация, подобная этой:

#import "MultiThreadFileWriter.h"

@implementation MultiThreadFileWriter

- (id)initWithOutputPath:(NSString *)aFilePath
{
    self = [super init];
    if (self) {
        i_fileLock = [[NSLock alloc] init];
        i_outputFile = fopen([aFilePath UTF8String], "w");
        if (!i_outputFile || !i_fileLock) {
            [self release];
            self = nil;
        }
    }
    return self;
}

- (void)dealloc
{
    [self close];
    [i_fileLock release];
    [super dealloc];
}

- (BOOL)writeBytes:(const void *)bytes ofLength:(size_t)length
    toFileOffset:(off_t)offset
{
    BOOL success;

    [i_fileLock lock];
    success = i_outputFile != NULL
        && fseeko(i_outputFile, offset, SEEK_SET) == 0
        && fwrite(bytes, length, 1, i_outputFile) == 1;
    [i_fileLock unlock];
    return success;
}

- (BOOL)writeData:(NSData *)data toFileOffset:(off_t)offset
{
    return [self writeBytes:[data bytes] ofLength:[data length]
        toFileOffset:offset
    ];
}

- (void)close
{
    [i_fileLock lock];
    if (i_outputFile) {
        fclose(i_outputFile);
        i_outputFile = NULL;
    }
    [i_fileLock unlock];
}
@end

замок можно было избежать по-разному. Использование Grand Central Dispatch и блоков для планирования операций поиска + записи в последовательной очереди будет работать. Другой способ-использовать обработчики файлов UNIX (POSIX) вместо стандартных c (open() и int вместо FILE * и fopen()), дублировать обработчик несколько раз (dup() функция), а затем помещая каждый из них в другое смещение файла, что позволяет избежать дальнейших операций поиска по каждой записи, а также блокировки, так как POSIX I/O является потокобезопасным. Однако обе реализации будут несколько более сложными, менее переносимыми, и не будет никакого ощутимого повышения скорости.


ответы, данные до сих пор есть некоторые явные недостатки:

  • файловый ввод-вывод с использованием системных вызовов определенно имеет некоторые недостатки в отношении блокировки.
  • кэширование частей в памяти приводит к серьезным проблемам в среде с ограниченной памятью. (т. е. любой компьютер)

потокобезопасный, эффективный, свободный от блокировки подход будет использовать отображения памяти, которая работает следующим образом:

  • создать результат файл (по крайней мере) общей необходимой длины
  • open() файл для чтения/записи
  • mmap() это в какое-то место в памяти. Файл теперь "живет" в памяти.
  • записать полученные детали в память со смещением вправо в файле
  • следите, если все части были получены (например, разместив некоторый селектор на основной поток для каждой части, полученной и сохраненной)
  • munmap() память и close() в файл

фактическая запись обрабатывается ядром - ваша программа никогда не будет выдавать write системный вызов любой формы. Отображение памяти обычно имеет небольшие недостатки и широко используется для таких вещей, как общие библиотеки.

обновление: кусок кода говорит более 1000 слов... Это mmap версия многопоточного файла на основе блокировки Mecki. Обратите внимание, что запись сводится к простому memcpy, который не может потерпеть неудачу(!!), так что нет BOOL success чтобы проверить. Производительность эквивалентна версии на основе блокировки. (проверено путем написания 100 блоков 1mb параллельно)

относительно комментария о" излишестве"mmap основанный подход: это использует меньше строк кода, не требует блокировки, с меньшей вероятностью блокирует запись, не требует проверки возвращаемых значений при записи. Единственным "излишеством" было бы то, что он требует от разработчика понимания другой концепции, чем старый добрый файл чтения/записи I/O.

в возможность чтения непосредственно в область памяти mmapped исключена, но довольно проста в реализации. Вы можете просто read(fd,i_filedata+offset,length); или recv(socket,i_filedata+offset,length,flags); прямо в файл.

@interface MultiThreadFileWriterMMap : NSObject
{
@private
    FILE * i_outputFile;
    NSUInteger i_length;
    unsigned char *i_filedata;
}

- (id)initWithOutputPath:(NSString *)aFilePath length:(NSUInteger)length;
- (void)writeBytes:(const void *)bytes ofLength:(size_t)length
      toFileOffset:(off_t)offset;
- (void)writeData:(NSData *)data toFileOffset:(off_t)offset;
- (void)close;
@end

#import "MultiThreadFileWriterMMap.h"
#import <sys/mman.h>
#import <sys/types.h>

@implementation MultiThreadFileWriterMMap

- (id)initWithOutputPath:(NSString *)aFilePath length:(NSUInteger)length
{
    self = [super init];
    if (self) {
        i_outputFile = fopen([aFilePath UTF8String], "w+");
        i_length = length;
        if ( i_outputFile ) {
            ftruncate(fileno(i_outputFile), i_length);
            i_filedata = mmap(NULL,i_length,PROT_WRITE,MAP_SHARED,fileno(i_outputFile),0);
            if ( i_filedata == MAP_FAILED ) perror("mmap");
        }
        if ( !i_outputFile || i_filedata==MAP_FAILED ) {
            [self release];
            self = nil;
        }
    }
    return self;
}

- (void)dealloc
{
    [self close];
    [super dealloc];
}

- (void)writeBytes:(const void *)bytes ofLength:(size_t)length
      toFileOffset:(off_t)offset
{
    memcpy(i_filedata+offset,bytes,length);
}

- (void)writeData:(NSData *)data toFileOffset:(off_t)offset
{
    memcpy(i_filedata+offset,[data bytes],[data length]);
}

- (void)close
{
    munmap(i_filedata,i_length);
    i_filedata = NULL;
    fclose(i_outputFile);
    i_outputFile = NULL;
}

@end

постройте объекты сегмента в очередь по мере их поступления в поток записи. Поток записи должен хранить список объектов вне порядка, чтобы фактическая запись на диск была последовательной. Если загрузка сегмента завершается неудачно, его можно вернуть в пул потоков загрузки для другой попытки (возможно, следует сохранить внутренний счетчик повторных попыток). Я предлагаю пул сегментных объектов, чтобы предотвратить одну или несколько неудачных загрузок одного сегмента, что приводит к использованию памяти при загрузке более поздних сегментов и добавил в список.