Синхронизация Fifo с несколькими потоками. Две записи в fifo из одного потока работают только со спящим режимом(1)

Я делаю приложение, которое вначале разветвляется на два процесса. Упрощенно, один из процессов постоянно читает из файла FIFO, а другой процесс время от времени записывает уведомления в файл FIFO. Теперь все работает нормально, если только процесс записи не вызывает метод записи быстро и последовательно. Затем из файла fifo записывается (или читается?) только первое уведомление.

Это код процесса чтения fifo:

void logging(){
    int fd1;

    char * myfifo = "/home/jens/Desktop/CLion_projects/Labo9/logfifo";
    mkfifo(myfifo, 0666);
    char str1[20];
    while(1) {
        pthread_mutex_lock(&lock_fifo);
        fd1 = open(myfifo,O_RDONLY);                            
        read(fd1, str1, 1000);

        printf("%s\n", str1);
        close(fd1);
        pthread_mutex_unlock(&lock_fifo);
    }
}

Это метод, который записывает в файл fifo (в другом процессе):

void write_to_fifo(char * string_to_write){
    int fd;
    char * myfifo = "/home/jens/Desktop/CLion_projects/Labo9/logfifo";
//    mkfifo(myfifo, 0666);                             (should this be on or not?)

    char * arr2 = string_to_write;
    fd = open(myfifo, O_WRONLY);
    write(fd, arr2, strlen(arr2)+1);
    close(fd);
}

Это два вызова, которые используют метод write_to_fifo(..):

    write_to_fifo("Connection to SQL server established.\n");
//    sleep(1);
    write_to_fifo("New table "TO_STRING(TABLE_NAME)" created.\n");

Первый всегда печатается правильно, второй работает только тогда, когда sleep(1) раскомментирован. Из-за этого, я предполагаю, что это как-то связано со временем. Было бы несложно оставить там sleep(1), если бы я не запускал несколько потоков одновременно. Я предполагаю, что запуск нескольких потоков делает время непредсказуемым, и вы не можете добавить строку sleep(1) между вызовами функций разных потоков.

  1. Почему эта программа работает только тогда, когда введена задержка?
  2. Это так и должно быть?
  3. Если нет, то как мне это преодолеть?

person Jens    schedule 10.08.2020    source источник


Ответы (1)


  1. Почему эта программа работает только тогда, когда введена задержка?

Потому что FIFO ориентированы на потоки, а не на сообщения. Небезопасно предполагать, что данные, записанные одним вызовом write(), будут считаны как единое целое любым вызовом read(). В вашем конкретном случае, если ваша функция write_to_fifo() вызывается несколько раз в быстрой последовательности, вы можете получить две или более записи, происходящие между чтениями, и в этом случае одно чтение может получить все данные из обеих записей, а не только из первого.

Кроме того, если это произойдет, то это будет скрыто от вас, потому что ваши записи (преднамеренно, кажется) включают терминаторы строк. Сколько бы считыватель ни читал, его printf() будет выводить данные только до первого терминатора. То есть не вижу оснований думать, что вы теряете сообщения в fifo; скорее, я уверен, что вы теряете их в читателе.

  1. Это так и должно быть?

Поведение, которое вы описываете, кажется мне последовательным, как описано выше.

  1. Если нет, то как мне это преодолеть?

Даже если поведение в некотором смысле является таким, каким оно должно быть, это не означает, что вы не можете получить поведение, которое вам больше нравится. Поскольку (почти наверняка) именно читатель теряет сообщения, вы можете исправить это, сделав читатель умнее. Вы должны быть в состоянии сделать это даже без модификации модуля записи, но модификация модуля записи может упростить задачу, и есть другие причины, по которым вы можете захотеть изменить его.

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

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

Но если вы хотите обрабатывать сообщения переменной длины в единицах сообщения, вам поможет лучший протокол. Например, отправляйте сообщения в виде сообщения фиксированной длины, за которым следует указанное количество байтов сообщения. Таким образом, программа чтения всегда знает, сколько байтов нужно прочитать (даже если для их получения необходимо использовать несколько вызовов read()).

В-третьих, читатели и писатели не должны постоянно открывать и закрывать fifo. Каждый процесс должен открыть его один раз и держать открытым столько времени, сколько необходимо. Любой из них или оба могут создать fifo один раз, хотя, если вы оба делаете это, вам нужно быть готовым к тому, что по крайней мере один из них не сможет этого сделать (из-за того, что другой сделал это первым). Несколько потоков записи могут совместно использовать один и тот же дескриптор файла, и на самом деле это часто предпочтительнее, чем несколько потоков, каждый из которых открывает один и тот же файл по отдельности. Вы могли бы подумать о том, чтобы потоки записи вызывали fsync() после каждого сообщения вместо закрытия файла, но это, вероятно, не нужно, если все они используют один и тот же FD.

В многопоточном случае, используя один и тот же FD для всех потоков, вам не нужно беспокоиться о том, что данные из одного вызова записи одного потока чередуются с данными из вызова записи другим потоком. Тем не менее, вы делаете должны знать, что если исходящее сообщение в конечном итоге будет разделено на более чем один вызов write, вы можете иногда получить промежуточную запись другого потока. Вы можете использовать мьютекс, чтобы гарантировать, что сообщения не будут разделены таким образом, когда они распределяются по нескольким записям.

Однако если есть только один читатель, то мне неясно, что дает использование мьютекса на этой стороне.

person John Bollinger    schedule 10.08.2020
comment
Здравствуйте, Джон, спасибо, что нашли время для написания такого подробного ответа. Я многому научился благодаря этому! У меня все еще есть вопрос о том, как вы должны выбирать длину «чтения» при чтении из fifo, когда записи fifo имеют переменную длину. Я использую разные строки, в которых используется '\n'. Когда часть следующей строки читается вместе с предыдущей, часть после \n обрезается. На данный момент я выбрал длину чтения = 1. Однако это приводит к большому количеству циклов. Можно ли сказать программе читать до определенного момента? Еще раз большое спасибо за этот ответ! - person Jens; 12.08.2020
comment
@Jens, нет, невозможно указать read() прекратить чтение, когда он достигнет определенного символа. Если вы (все еще) обрезаете данные, то я подозреваю, что вы (все еще) записываете терминаторы строк в FIFO, а затем читаете их обратно. Извините, если я был неясен, но идея, которую я представил о том, чтобы авторы включали новые строки в то, что они пишут, заключалась в том, что они будут делать это вместо включения разделителей строк в то, что они пишут. - person John Bollinger; 12.08.2020
comment
И, конечно же, у вас есть другие варианты, такие как альтернативный протокол, описанный в этом ответе. В целом, хотя отправка нулевых байтов по сети не является ошибочной, я рекомендую избегать этого в вашем приложении. - person John Bollinger; 12.08.2020
comment
я думал, что под терминаторами строк вы имели в виду '\ 0', но, поскольку я не помещаю их вручную в char *, я думал, что они не создадут проблем. Должен ли я вручную избавиться от них, удалив последнюю запись символа *? - person Jens; 12.08.2020