在多个线程的文件中写入

时间:2012-02-16 22:11:26

标签: objective-c multithreading cocoa thread-safety

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

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

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

3 个答案:

答案 0 :(得分:10)

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

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

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

  • 创建(至少)所需总长度的结果文件
  • open()用于读/写的文件
  • mmap()它在记忆中的某个地方。该文件现在“存在”内存中。
  • 将收到的部分写入内存中文件
  • 中的右偏移处
  • 跟踪是否已收到所有作品(例如,通过在主要主题上为每个收到并存储的作品张贴一些选择器)
  • munmap()内存和close()文件

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

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

答案 1 :(得分:4)

将段对象在接收到编写器线程时进行排队。 writer-thread应保留一个无序对象列表,以便实际的磁盘写入是顺序的。如果段下载失败,则可以将其推回到下载线程池以进行另一次尝试(可能应保留内部重试计数)。我建议使用一个段对象池来防止一个段的一个或多个失败下载导致内存使用失控,因为后续段被下载并添加到列表中。

答案 2 :(得分: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

与此类似的实现:

#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和Blocks来安排串行队列上的搜索和写入操作将起作用。另一种方法是使用UNIX(POSIX)文件处理程序而不是标准C语言处理程序(open()int而不是FILE *fopen()),多次复制处理程序( dup()函数)然后将它们中的每一个放置到不同的文件偏移量,这避免了在每次写入时进一步搜索操作以及锁定,因为POSIX I / O是线程安全的。但是,这两种实现方式都会更复杂,便携性更低,并且没有可测量的速度改进。