Как программно прекратить чтение со стандартного ввода?

fgetc() и другие функции ввода могут возвращать значение, когда в файловом дескрипторе нет данных. Это можно смоделировать для консольных приложений, читающих со стандартного ввода, набрав Ctrl-D на клавиатуре (по крайней мере, в Unix). Но как это сделать программно? Например, как вернуться из fgetc() в потоке чтения в следующем коде (примечание: игнорировать возможное состояние гонки)?

#include <pthread.h>
#include <stdio.h>

void* reader()
{
    char read_char;
    while((read_char = fgetc(stdin)) != EOF) {
        ;
    }

    pthread_exit(NULL);
}

int main(void)
{
    pthread_t thread;
    pthread_create(&thread, NULL, reader, NULL);

    // Do something so the fgetc in the reader thread will return

    pthread_exit(NULL);
}

Спасибо!


person ceztko    schedule 15.11.2010    source источник
comment
Я думаю, вы просто хотите close() на другом конце канала, но ваш пример на самом деле не имеет смысла, поскольку основной поток не контролирует другой конец стандартного ввода.   -  person Alex Jasmin    schedule 15.11.2010
comment
Я пробовал это несколько недель назад, но не нашел способа узнать другой конец стандартного ввода. Есть ли другой способ достичь той же цели? Может быть, я что-то упускаю: мне просто нужен способ не блокировать потоки при вводе с консоли.   -  person ceztko    schedule 15.11.2010
comment
Другой конец стандартного ввода обычно является клавиатурой вашего терминала. Возможно, вам нужен неблокирующий ввод-вывод? Из вашего вопроса трудно сказать. Вы должны объяснить, что вы пытаетесь сделать более подробно.   -  person Alex Jasmin    schedule 15.11.2010
comment
Нет, мне не нужен неблокирующий ввод-вывод. Не в том смысле, что я должен опрашивать ввод с тайм-аутами с помощью select() или poll(), по крайней мере: это было бы неправильно, чем пытаться закрыть устройство с клавиатурой терминала. Мне кажется, что я неправильно понял, пытаясь получить доступ к стандартному вводу из разных потоков. Это, вероятно, неправильно, и я должен избегать этого, сохраняя состояние моего приложения в области чтения ввода, чтобы я мог легко отслеживать, когда ввод должен быть обработан или нет.   -  person ceztko    schedule 15.11.2010
comment
Между прочим, Windows-эквивалентом UNIX-команды control-D является control-Z.   -  person Harry Johnston    schedule 05.05.2014


Ответы (5)


Кажется, вы хотите, чтобы потоки перестали блокироваться на fgetc(stdin), когда происходит какое-то событие, чтобы вместо этого обработать это событие. Если это так, вы можете select() использовать как stdin, так и какой-либо другой канал сообщений, чтобы поток мог обрабатывать ввод из обоих:

fd_set descriptor_set
FD_ZERO(&descriptor_set); 
FD_SET(STDIN_FILENO, &descriptor_set); 
FD_SET(pipefd, &descriptor_set); 

if (select(FD_SETSIZE, &descriptor_set, NULL, NULL, NULL) < 0) 
{ 
  // select() error
} 

if (FD_ISSET(STDIN_FILENO, &descriptor_set)) {
  // read byte from stdin
  read(STDIN_FILENO, &c, 1);
}

if (FD_ISSET(pipefd, &descriptor_set)) 
  // Special event. Do something else

Также обратите внимание, что только один поток в вашем процессе должен читать из stdin.

person Alex Jasmin    schedule 15.11.2010
comment
Да, это именно тот ответ на мой вопрос, супер спасибо! Это может быть излишним в моих реальных потребностях, но в любом случае это путь, если кому-то нужен подобный шаблон. Я был близок к этому решению, когда пробовал это несколько недель назад, но совершенно упустил возможность select() отслеживать более одного файлового дескриптора. В моем предыдущем комментарии о том, что я не использую select() или poll(), я был против использования тайм-аутов, но, похоже, вы прекрасно поняли суть. Спасибо еще раз! - person ceztko; 15.11.2010

Вы можете либо «закрыть» стандартный ввод, либо подключить стандартный ввод к «/dev/null» («NUL:» в Windows) с помощью freopen(), либо вы можете подключить стандартный ввод к «/dev/zero».

Если вы закроете его, каждый вызов функции завершится ошибкой, потому что файловый поток недействителен. Если вы подключите его к нулевому источнику данных, все чтения завершатся ошибкой и немедленно вернут EOF. Если вы подключите его к нулевому источнику данных, каждое чтение будет успешным и вернет соответствующее количество нулевых байтов.

Вполне возможно, что один из них будет достаточно соответствовать вашим потребностям. Если нет, то вам, вероятно, нужно дать нам более подробное объяснение того, что вам действительно нужно.

person Jonathan Leffler    schedule 15.11.2010
comment
Некоторое время назад я безуспешно пробовал это, но кажется, что это не переносимо, и на самом деле это не работает в Linux. Попытка закрыть stdin (или любой другой файловый дескриптор), пока fgetc() ожидает данных, здесь блокируется. Обсуждалось здесь: lkml.indiana.edu/hypermail/linux/kernel/ 0106.0/0768.html со ссылкой на select() и ответ от разработчиков Linux: неопределенное поведение. - person ceztko; 15.11.2010
comment
@ceztko: закрытие стандартного ввода в одном потоке, пока другой поток читает - да, это не будет надежным ... Должно быть, я неправильно понял вопрос. - person Jonathan Leffler; 15.11.2010

С помощью POSIX вы можете сигнализировать потоку, чьи примитивы (например, «чтение») блокируются, и если вы настроили обработчик сигнала бездействия со сброшенным битом SA_RESTART, то примитивы завершатся ошибкой EINTR. Вот рабочая версия оригинала:

#include <pthread.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

static void SignalHandler(int signum)
{
}

void* reader(void *arg)
{
    char read_char;
    while((read_char = fgetc(stdin)) != EOF) {
        ;
    }
    printf("leaving reader\n");
    return NULL;
}

int main(int argc, const char * argv[])
{
    struct sigaction action;
    memset(&action, 0, sizeof(action));  // SA_RESTART bit not set
    action.sa_handler = SignalHandler;
    sigaction(SIGUSR1, &action, NULL);

    pthread_t thread;
    pthread_create(&thread, NULL, reader, NULL);

    sleep(1); // time to start reader thread
    // Do something so the fgetc in the reader thread will return
    pthread_kill(thread, SIGUSR1);
    sleep(1); // time to exit reader thread; could join it if set up

    return 0;
}
person slimcity5    schedule 22.05.2018
comment
Я не проверял, но думаю, ваш код будет работать. Тем не менее, я думаю, что ваш подход немного груб: вы сигнализируете потоку и, в зависимости от сложности функции reader(), вы можете вызвать непреднамеренные точки выхода, которые не обязательно были предназначены, из-за побочных эффектов в большой области кода (примитивы ядра ) у вас нет контроля. Я думаю, что подход, предложенный в отмеченном ответе, гораздо более детализирован и позволяет точно вытеснять поток в предполагаемой точке выхода. Смотрите также мой ответ с полным примером. - person ceztko; 23.05.2018
comment
@ceztko: Да, мой подход проще, но гораздо менее целенаправленный. Мне непонятен момент с отмеченным ответом: можете ли вы использовать его с функциями более высокого уровня, такими как fscanf? - person slimcity5; 23.05.2018
comment
По-видимому, вы можете использовать fscanf с каналом с некоторыми предостережениями - person ceztko; 23.05.2018

Александр опубликовал правильное решение. Его ответ отвечает именно на вопрос, который я задал. Это следует за простым самокомпилируемым кодом, основанным на его подсказках:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

static int pipe_fds[2];

void* user_interaction()
{
    char read_char;
    
    fd_set descriptor_set;
    FD_ZERO(&descriptor_set); 
    FD_SET(STDIN_FILENO, &descriptor_set); 
    FD_SET(pipe_fds[0], &descriptor_set);

    while(1)
    {
        if (select(FD_SETSIZE, &descriptor_set, NULL, NULL, NULL) < 0) {
            // select() error
        }

        if (FD_ISSET(STDIN_FILENO, &descriptor_set)) {
            // read byte from stdin
            read(STDIN_FILENO, &read_char, 1);
            // Re-set the selected file descriptor so it can
            // be signaled again
            FD_SET(STDIN_FILENO, &descriptor_set);
        }

        if (FD_ISSET(pipe_fds[0], &descriptor_set))
            // Special event. break
            break;
    }
    
    pthread_exit(NULL);
}

int main(void)
{
    pipe(pipe_fds);
    
    pthread_t thread;
    pthread_create(&thread, NULL, user_interaction, NULL);
    
    // Before closing write pipe endpoint you are supposed
    // to do something useful
    sleep(5);

    close(pipe_fds[1]);
    
    pthread_join(thread, NULL);
    
    pthread_exit(NULL);
}
person ceztko    schedule 15.11.2010

Я попытался использовать код из этого ответа в слегка измененном виде:

void *user_interaction()
{
    char ch;
    int rv;
    fd_set set;

    FD_ZERO(&set);
    FD_SET(STDIN_FILENO, &set);
    FD_SET(pipe_fds[0], &set);

    while (1)
    {
        rv = select(pipe_fds[0] + 1, &set, NULL, NULL, NULL);
        if (rv < 0)
        {
            printf(">>> select(): error occurred, %d\n", rv);
            break;
        }
        
        if (FD_ISSET(pipe_fds[0], &set))
        {
            printf(">>> pipe_fds[0]: is ready\n");
            break;
        }

        if (FD_ISSET(STDIN_FILENO, &set))
        {
            read(STDIN_FILENO, &ch, 1);
            write(STDOUT_FILENO, &ch, 1);
        }
    }

    pthread_exit(NULL);
}

но не получил ожидаемого поведения. При выполнении, как показано ниже:
$ echo -n 1 | ./a.out

мой терминал визуализировался с 1 в бесконечном цикле, и select() никогда не сообщал о готовности канала (т.е. даже после close()ing его в основном потоке).

Немного поэкспериментировав, я понял, что вам нужно переместить FD_ZERO/FD_SET внутри цикла, чтобы select() работало так, как нужно:

void *user_interaction()
{
    char ch;
    int rv;
    fd_set set;

    while (1)
    {
        FD_ZERO(&set);
        FD_SET(STDIN_FILENO, &set);
        FD_SET(pipe_fds[0], &set);

        rv = select(pipe_fds[0] + 1, &set, NULL, NULL, NULL);
        if (rv < 0)
        {
            printf(">>> select(): error occurred, %d\n", rv);
            break;
        }
        
        if (FD_ISSET(pipe_fds[0], &set))
        {
            printf(">>> pipe_fds[0]: is ready\n");
            break;
        }

        if (FD_ISSET(STDIN_FILENO, &set))
        {
            read(STDIN_FILENO, &ch, 1);
            write(STDOUT_FILENO, &ch, 1);
        }
    }

    pthread_exit(NULL);
}
person Zdzisław Śliwiński    schedule 31.07.2020
comment
Упростите свой ответ, удалив код, который не работает, и оставьте только хорошую часть. Также вы можете прокомментировать исходный ответ и указать, что было не так. В этом FD_ZERO не является строго необходимым, но необходима переустановка сигнального дескриптора. Я исправлю в своем ответе дескриптор пользовательских данных (другой дескриптор предназначен для управления/выхода из программы). - person ceztko; 31.07.2020
comment
Хотел прокомментировать, но моя репутация здесь была низкой... :( Рад, что вам нравятся улучшения! - person Zdzisław Śliwiński; 31.07.2020