在多个线程的文件中写入

Syl*_*ier 7 cocoa multithreading objective-c thread-safety

我正在Objective-C中编写一个下载管理器,它可以同时从多个段下载文件,以提高速度.文件的每个部分都在一个线程中下载.

起初,我想将每个段写在不同的文件中,并在下载结束时将所有文件放在一起.但由于种种原因,这不是一个好的解决方案.

所以,我正在寻找一种在特定位置写入文件并且能够处理多个线程的方法,因为在我的应用程序中,每个段都被下载到一个线程中.在Java中,我知道FileChannel完美的诀窍,但我不知道Objective-C.

mvd*_*vds 10

到目前为止给出的答案有一些明显的缺点:

  • 使用系统调用的文件i/o肯定在锁定方面有一些缺点.
  • 内存中的缓存部分会导致内存受限环境中的严重问题.(即任何电脑)

线程安全,高效,无锁的方法是使用内存映射,其工作方式如下:

  • 创建(至少)所需总长度的结果文件
  • open() 用于读/写的文件
  • mmap()它在记忆中的某个地方.该文件现在"存在"内存中.
  • 将接收到的部分写入文件中右侧偏移的内存中
  • 如果已收到所有作品,请跟踪(例如,通过在主线程上为每个收到并存储的作品发布一些选择器)
  • munmap()内存和close()文件

实际写入由内核处理 - 您的程序永远不会发出write任何形式的系统调用.内存映射通常没什么缺点,并且广泛用于共享库之类的东西.

更新:一段代码说超过1000字......这是mmapMecki基于锁的多线程文件编写器的版本.请注意,写入简化为简单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
Run Code Online (Sandbox Code Playgroud)


Mec*_*cki 3

永远不要忘记,Obj-C 基于普通 C,因此我只需编写一个自己的类,它使用标准 C API 处理文件 I/O,它允许您将当前写入位置放置在新文件中的任何位置,甚至远远超出当前文件大小(缺少的字节用零字节填充),以及根据需要向前和向后跳跃。实现线程安全的最简单方法是使用锁,这不一定是最快的方法,但在您的具体情况下,我敢打赌瓶颈肯定不是线程同步。该类可以有这样的标题:

@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
Run Code Online (Sandbox Code Playgroud)

以及与此类似的实现:

#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
Run Code Online (Sandbox Code Playgroud)

可以通过多种方式避免锁定。使用 Grand Central Dispatch 和 Blocks 来安排串行队列上的查找 + 写入操作是可行的。另一种方法是使用 UNIX (POSIX) 文件处理程序而不是标准 C 文件处理程序(open()用 andint代替FILE *and fopen()),多次复制处理程序(dup()函数),然后将每个处理程序放置到不同的文件偏移量,这避免了进一步查找操作每次写入和锁定,因为 POSIX I/O 是线程安全的。然而,这两种实现都会稍微复杂一些,可移植性较差,并且不会有可测量的速度改进。