在fopen + mode中从多个线程/进程写入锁定文件之前是否需要同步/刷新?

gre*_*reg 1 c concurrency fopen locking file

我正在从多个线程对单个文件执行I/O. foo通过建议文件锁(flock(2)LOCK_EX)来控制对此共享文件的访问. foofopen(3)模式打开了a+. a+之所以被选中,是因为文件说明:

对文件的后续写入将始终以当前文件末尾结束,无论是否有任何干预fseek(3)或类似.

简化后,操作将开始:

FILE *fp = fopen("foo", "a+");
...spawn threads...
Run Code Online (Sandbox Code Playgroud)

写作将继续:

flock(fileno(fp), LOCK_EX);
fwrite(buffer, buffer_size, 1, fp);
flock(fileno(fp), LOCK_UN);
Run Code Online (Sandbox Code Playgroud)

我目前没有任何fflush(3)fsync(2)电话fwrite(3),我想知道我是否应该.fopen(3) a+在计算"当前EOF"时,模式是否会考虑多个线程命中文件?我知道,flock(2)当有出色的I/O时,给我锁定可能没有问题.

在我的有限测试中(写入非常长的ASCII文本行,然后在多个线程中使用换行多秒,然后确保生成的文件中每行的字符数相等),我没有看到任何"损坏"使用fflush(3)fsync(2).它们的存在大大降低了I/O性能.

tl; dr:使用文件锁时,是否需要在打开a+模式下写入多个线程之间的共享文件之前刷新流?多个叉子/不同的机器写入文件并行文件系统?

可能相关: 为什么在读/写"+"模式下读写之间总是需要fseek或fflush

Die*_*Epp 5

那是一种错误的锁定. flock进行进程间的锁定,而不是在同一进程中的线程之间.来自man 2 flock:

A call to flock() may block if an incompatible lock is held by  another
process.   To  make  a  nonblocking request, include LOCK_NB (by ORing)
with any of the above operations.

强调补充说.和...

A process may only hold one type of lock (shared  or  exclusive)  on  a
file.   Subsequent flock() calls on an already locked file will convert
an existing lock to the new lock mode.

您想要使用flockfile(或者另外,如果使用多个进程).flockfile用于控制对FILE *多个线程的访问的函数.从手册页:

The stdio functions are thread-safe.  This is achieved by assigning  to
each  FILE object a lockcount and (if the lockcount is nonzero) an own?
ing thread.  For each library call, these functions wait until the FILE
object  is no longer locked by a different thread, then lock it, do the
requested I/O, and unlock the object again.

(Note: this locking has nothing to do with the  file  locking  done  by
functions like flock(2) and lockf(3).)

像这样:

// in one of the threads...
flockfile(fp);
fwrite(..., fp);
funlockfile(fp);
Run Code Online (Sandbox Code Playgroud)

好消息是,glibc如果在每个关键部分只有一个来自stdio.h的函数调用,则不需要锁定文件,因为glibc有一个fwrite锁定.但是在其他平台上并非如此,锁定文件肯定没有坏处.因此,如果你在Linux上运行,你永远不会注意到flock它没有做你想要的,因为fwrite它是自动的.

关于追加模式:使用追加模式编写时不需要额外的刷新,除非您希望确保在打开相同文件的不同进程之间进行排序(或者对同一文件使用多个句柄的进程).除非您正在从文件中读取,否则不需要"a +"模式.

演示 flock

如果你不相信我flock不使用相同的文件描述符在线程之间提供线程安全,那么这是一个演示程序.

#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <sys/file.h>

static FILE *fp;
static pthread_mutex_t mutex;
static pthread_cond_t cond;
int state;

static void fail_func(int code, const char *func, int line)
{
    fprintf(stderr, "%s:%d: error: %s\n", func, line, strerror(code));
    exit(1);
}

#define fail(code) fail_func(code, __FUNCTION__, __LINE__)

void *thread1(void *p)
{
    int r;

    // Lock file (thread 2 does not have lock yet)
    r = pthread_mutex_lock(&mutex);
    if (r) fail(r);
    r = flock(fileno(fp), LOCK_EX);
    if (r) fail(errno);
    puts("thread1: flock successful");
    state = 1;
    r = pthread_mutex_unlock(&mutex);
    if (r) fail(r);

    // Wake thread 2
    r = pthread_cond_signal(&cond);
    if (r) fail(r);

    // Wait for thread 2
    r = pthread_mutex_lock(&mutex);
    if (r) fail(r);
    while (state != 2) {
        r = pthread_cond_wait(&cond, &mutex);
        if (r) fail(r);
    }
    puts("thread1: exiting");
    r = pthread_mutex_unlock(&mutex);
    if (r) fail(r);

    return NULL;
}

void *thread2(void *p)
{
    int r;

    // Wait for thread 1
    r = pthread_mutex_lock(&mutex);
    if (r) fail(r);
    while (state != 1) {
        r = pthread_cond_wait(&cond, &mutex);
        if (r) fail(r);
    }

    // Also lock file (thread 1 already has lock)
    r = flock(fileno(fp), LOCK_EX);
    if (r) fail(r);
    puts("thread2: flock successful");

    // Wake thread 1
    state = 2;
    puts("thread2: exiting");
    r = pthread_mutex_unlock(&mutex);
    if (r) fail(r);
    r = pthread_cond_signal(&cond);
    if (r) fail(r);

    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t t1, t2;
    void *ret;
    int r;

    r = pthread_mutex_init(&mutex, NULL);
    if (r) fail(r);
    r = pthread_cond_init(&cond, NULL);
    if (r) fail(r);
    fp = fopen("flockfile.txt", "a");
    if (!fp) fail(errno);
    r = pthread_create(&t1, NULL, thread1, NULL);
    if (r) fail(r);
    r = pthread_create(&t2, NULL, thread2, NULL);
    if (r) fail(r);
    r = pthread_join(t1, &ret);
    if (r) fail(r);
    r = pthread_join(t2, &ret);
    if (r) fail(r);
    puts("done");
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

在我的系统上,它产生以下输出:

thread1: flock successful
thread2: flock successful
thread2: exiting
thread1: exiting
done

请注意,线程1不会释放flock,并且线程2无论如何都能够获取它.条件变量的使用确保线程1在线程2获得锁定之前不会退出. 这正是flock手册页所说的,因为flock说锁是每个文件和每个进程但不是每个线程.

原子附加到文件的摘要

为了在进程和线程之间进行原子写入,您可以执行以下两项简单操作之一:

  • 使用write和写入不超过PIPE_BUF字节. 在我的系统中PIPE_BUF定义<limits.h>为4096.如果文件描述符在O_APPEND模式下打开,那么无论是谁写入文件(线程和/或进程),写入都将自动进入文件的末尾.

  • 使用writeflock.如果您一次写入多于PIPE_BUF字节,则这是所有写入的唯一选项.同样,如果文件在O_APPEND模式下打开,则字节将转到文件的末尾.这将以原子方式发生,但仅限于每个人的观点flock.

另外,

  • 如果您在线程之间使用<stdio.h>和共享FILE *,则还需要flockfile从每个线程调用.如果您使用较低级别的POSIX API(open/ write/ etc),则不需要这样做.如果您使用glibc并且每次写入都是单个函数调用(例如,您希望以原子方式fputs),则也不需要这样做.

  • 如果您只使用一个过程,flock则不需要.