Нужна ли синхронизация / очистка перед записью в заблокированный файл из нескольких потоков / процессов в режиме fopen + - PullRequest
1 голос
/ 03 января 2012

Я выполняю ввод-вывод в один файл из нескольких потоков.Доступ к этому общему файлу foo контролируется с помощью принудительной блокировки файлов (flock(2) с LOCK_EX).foo был открыт в режиме fopen(3) a+.a+ было выбрано из-за документации, в которой указано:

Последующие записи в файл всегда будут заканчиваться текущим концом файла, независимо от какого-либо промежуточного fseek(3) или подобного.

Упрощенно, операции начнутся:

FILE *fp = fopen("foo", "a+");
...spawn threads...

Запись будет продолжена:

flock(fileno(fp), LOCK_EX);
fwrite(buffer, buffer_size, 1, fp);
flock(fileno(fp), LOCK_UN);

В настоящее время у меня нет вызовов fflush(3) или fsync(2) доfwrite(3) и мне интересно, если я должен.Учитывает ли режим fopen(3) a+ несколько потоков, попадающих в файл при вычислении «текущего EOF»?Я знаю, что flock(2), вероятно, не имеет проблем с предоставлением мне блокировки, пока имеется выдающийся ввод / вывод.

В моих ограниченных тестах (пишите очень длинные строки текста ASCII, за которым следует новая строка в нескольких потоках в течение многих секунд, затем убедитесь, что количество символов в каждой строке в результирующем файле равны), я не видел никакого "искажения", когда не используется fflush(3) или fsync(2).Их присутствие значительно снижает производительность ввода-вывода.

tl; dr: при использовании блокировок файлов необходимо ли очищать поток перед записью в общий файл между несколькими потоками, открытыми в режиме a+?Несколько вилок / разных машин записывают в файл параллельную файловую систему?

Возможно, связано: почему всегда требуются fseek или fflush между чтением и записью в режимах чтения / записи "+"

Ответы [ 2 ]

5 голосов
/ 03 января 2012

Это неправильный вид блокировки. flock - это только для блокировки между процессами, а не между потоками в одном и том же процессе. От man 2 flock:

A call to flock() may block if an incompatible lock is held by  <b>another
process.</b>   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.   <b>Subsequent flock() calls on an already locked file will convert
an existing lock to the new lock mode.</b>

Вместо этого вы хотите использовать 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);

Хорошая новость заключается в том, что на glibc вам не нужно блокировать файл, если у вас есть только один вызов функции из stdio.h в каждом критическом разделе, поскольку glibc имеет fwrite, который блокирует. Но это не верно для других платформ, и, конечно, это не повредит блокировать файл. Поэтому, если вы работаете в Linux, вы никогда бы не заметили, что flock делает не то, что вам нужно, поскольку fwrite делает это автоматически.

О режиме добавления: Вам не нужны дополнительные очистки при записи в режиме добавления, если только вы не хотите обеспечить упорядочение между различными процессами, в которых открыт один и тот же файл (или одним процессом с несколькими дескрипторами для одного файл). Вам не нужен режим «+», если вы не читаете из файла.

Демонстрация 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;
}

В моей системе выдает следующий вывод:

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, тогда запись будет происходить атомарно до конца файла, независимо от того, кто еще пишет в файл (темы и / или процессы).

  • Используйте write и flock. Если вы когда-либо записываете больше, чем PIPE_BUF байт за раз, это единственный вариант для всех записей. Опять же, если файл открыт в режиме O_APPEND, то байты будут идти до конца файла. Это произойдет атомарно, но только с точки зрения каждого с flock.

Дополнительно

  • Если вы используете <stdio.h> и совместно используете FILE * между потоками, вам также нужно будет вызывать flockfile из каждого потока. В этом нет необходимости, если вы используете низкоуровневый API POSIX (open / write / etc). Это также не требуется, если вы используете glibc и каждая запись представляет собой один вызов функции (например, вы хотите атомарно fputs).

  • Если вы используете только один процесс, flock не требуется.

1 голос
/ 06 июля 2012

Ответ выше не совсем правильный.

  1. write () в файл с режимом добавления является атомарным между многопоточностью и многопроцессорностью, независимо от того, сколько байтов записанов одно время.См. Стандарт: http://pubs.opengroup.org/onlinepubs/009695399/functions/write.html Если установлен флаг O_APPEND флагов состояния файла, смещение файла должно быть установлено в конец файла перед каждой записью, и между операциями модификации файла не должно бытьизменяя смещение файла и операцию записи.

  2. если write () в FIFO или канал с режимом добавления, PIPE_BUF ограничивает максимальный размер атомарной записи.

  3. библиотека stdio не гарантирует многопотоковую или многопоточную атомарную запись дополнения.Поскольку каждый FILE * имеет свой собственный буфер.

  4. flockfile работает только тогда, когда 1. Только один процесс обрабатывает файл 2. Многопоточность записывает файл с одним FILE *.

  5. Таким образом, когда многопроцессорным или многопоточным нужно писать файл с функциями stdio, единственным выбором является использование консультативной блокировки, flock работает только под linux, а fcntl переносим.

...