Чистый способ остановить / завершить поток, ожидающий на стандартный ввод в C ++ - PullRequest
0 голосов
/ 17 марта 2020

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

Есть ли более чистый / лучший способ, чем использование прерываний?

Ответы [ 4 ]

0 голосов
/ 18 марта 2020

Некоторые общие мысли:

Модель актера

Хорошо блокировать ожидание ввода от stdin. То же самое относится и к сокету UDP, на который вы намекали.

И stdin, и сокет являются потоками, которые вы, вероятно, разбиваете на отдельные сообщения. Но способ ожидания данных из файловых дескрипторов - это select (), epoll () или аналогичный (в зависимости от вашей платформы; заметьте, Windows дает вам выбор, который работает только с сокетами ...).

В любом случае, я хочу сказать, что, направляясь к использованию select () или epoll (), вы направляетесь к архитектуре Actor Model. Вопрос в том, действительно ли один go для этой целиком, по всему приложению? Я часто обнаруживал, что самый простой ответ - «да».

Это хорошо, потому что тогда ваш поток может выбрать () как для стандартного ввода, так и, скажем, для канала, в который основной поток напишет сообщение "выход". Когда поток обнаруживает, что канал готов к чтению, он читает его, видит сообщение "выход" и полностью закрывается, все аккуратно и аккуратно. Поток будет читать stdin только тогда, когда select () говорит, что есть что прочитать.

Подход "все это дескриптор файла", используемый * nixes, позволяет очень легко охватить практически любой мыслимый источник данных в select () или epoll ().

Linux даже делает сигналы доступными на ФД в эти дни. Гораздо проще считывать сигналы с fd и обрабатывать их синхронно в главном l oop, а не асинхронно в обработчике (который на самом деле мало что может сделать).

Reactor vs Proactor

Архитектура модели актера: Реакторы . У вашего процесса / потоков есть все oop, с чем-то вроде select () в верхней части l oop, и вы читаете ввод с того места, которое готово для чтения, и обрабатываете этот ввод соответственно. (Другие архитектуры, которые являются Reactors, включают в себя последовательные процессы связи, как в Rust и Go -lang, erlang тоже.)

Альтернативой является Proactors , то есть проактивное решение, что делать в продвижение любого входа, появляющегося. Это твердо в стране обратных вызовов, фьючерсов, асинхронных и т. Д. c. Ваш неблокирующий ответ cin выше - proactor.

В наши дни очень много вещей - это Proactor; Windows есть, Boost ASIO есть (потому что Windows есть) и так далее.

Старайтесь не смешивать Proactor и Reactor

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

Это означает, что нужно очень тщательно обдумать заранее (например, придется ли мне делать это на Windows, или мне когда-либо придется переносить его на Windows и т. Д. c).

Мои предпочтения

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

В Reactor приложение никогда не будет вызывать функцию чтения, пока не узнает, что есть что-то прочитать, а состояние потока таково, что должно произойти чтение (например, команда "quit" не получена) .

Неэквивалентность Pro / Reactor

Другой аспект заключается в том, что возможно реализовать поведение Proactor поверх инфраструктуры Reactor, но в действительности это невозможно.

Хорошие примеры этого включают ZMQ. ZMQ - это в основном архитектура модели Actor, Reactor, построенный на основе zmq_poll (). Для ZMQ имеется множество Windows креплений, которые представляют фасад в стиле Proactor.

ZMQ может работать только на Windows для сокетов (потому что Windows дает вам select () только для сокетов), но не будет работать на Windows каналах (нет выбора () для каналов). В Unix каналы поддерживаются транспортом ip c: //. Отсутствие каналов не так уж плохо в процессе на Windows, так как транспорт inpro c: // работает.

Аналогично, Boost ASIO является проактором просто из-за сложности реализации реактора на Windows, в то же время охватывая полный набор IP C, таких как каналы, сокеты и последовательные порты на Windows (что-то, что ZMQ выбрал не делать). На * nix, Boost ASIO представляет фронтальный процессор, но под капотом он реализован с использованием epoll () ...

ZMQ

Говоря о ZMQ, если вы работая над * nix, я настоятельно рекомендую использовать ZMQ в качестве библиотеки IP C. Он делает фантастическую c работу по созданию таких вещей, как ip c, по-настоящему простыми в использовании сокетами, и вы можете легко интегрировать такие вещи, как ожидание от обычных файловых дескрипторов (например, stdin), в вызов zmq_poll (). Более того, многие другие фреймворки, например, GUI фреймворки, позволяют вам использовать fd в качестве входных данных в их собственных циклах событий, и это может включать в себя fd «что-то случилось», которое дает вам ZMQ. Таким образом, относительно просто включить обработку ZMQ, встроенную в приложение gui.

Если вы используете его на Windows, будет невозможно интегрировать его с stdin в настоящий стиль реактора.

Общая история

Когда ребята-cygwin пришли к реализации select () для своей библиотеки на Windows, они столкнулись с ужасом, который заключался в невозможности получить правильная блокировка select (), которая может ожидать сокеты, последовательные порты, stdin, pipe и т. д. c. В конце концов, они реализовали это, имея поток на дескриптор файла, не относящийся к сокету, вращаясь в al oop, проверяя дескриптор устройства Windows, чтобы увидеть, не появилось ли что-нибудь. Это было крайне неэффективно.

Boost ASIO вышел proactor специально из-за желания заставить Boost работать на Windows.

Windows (кроме сокетов) с самого начала был нереформированным проактором, вероятно, вплоть до ядра и драйверов устройств. Однако в первой версии WSL (в настоящее время WSL довольно популярен) реализован системный вызов Linux shim; т. е. программа Linux, выполняющая вызов select (), в конечном итоге вызовет ядро ​​NT для эквивалентной функции.

Это означает, что каналы, по крайней мере внутри WSL 1.0, будут работать в select (), но это будет вызов ядра NT, реализующий его. Это означает, что некоторые элементы реактора куда-то попали в Windows, но не были выставлены на уровне Win32 / C, C ++.

Будучи решительно проактором, Windows очень напоминает древние Unixes, которые также не могли делать select ().

0 голосов
/ 17 марта 2020

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

Когда возвращается функция main во время выполнения C ++ std::exit. std::exit завершает весь процесс со всеми его потоками. std::exit может быть вызван из любого, чтобы завершить весь процесс.

0 голосов
/ 17 марта 2020

В соответствии с этим вопросом Я попытался построить неблокирующий cin, используя std :: asyn c:

#include <iostream>
#include <future>
#include <thread>
#include <chrono>

static std::string getAnswer()
{    
    std::string answer;
    std::cout << "waiting on stdin" << std::endl;
    std::cin >> answer;
    return answer;
}

int main()
{

    std::chrono::seconds timeout(5);
    std::string answer = "default"; //default to maybe
    while(true) {
        std::cout << "new loop" << std::endl << std::flush;
        std::future<std::string> future = std::async(getAnswer);
        if (future.wait_for(timeout) == std::future_status::ready) {
            answer = future.get();
        }
        std::cout << "Input was: " << answer << std::endl;
    }

    exit(0);
}

Что я получаю на stdout:

new loop                                                                                                      
waiting on stdin                                                                                              
Input was: default // after 5 sec

l oop не запускается снова. Вместо этого, когда я ввожу что-то на клавиатуре, l oop запускается снова. Может ли кто-нибудь объяснить мне это поведение?

0 голосов
/ 17 марта 2020

А как насчет очереди задач?

Псевдокод:

пользовательский поток ввода

void user_input_thread_function()
{
    Task* task;

    for(;;)
    {
        queue.wait_dequeue(task);

        if (!task)
        {
            // task is nullptr, a signal to stop gracefully
            break;
        }

        // main thread did not yet ended and sent a valid task
        // do something with task

        delete task;
    }

    // do necessary things before stopping
}

основной поток

// queue should be visible to both threads

QueueType queue;

int main()
{
    thread user_input_thread(user_input_thread_function);

    for(;;)
    {
        queue.enqueue(new Task("data"));

        if (input exhausted)
            queue.enqueue(nullptr);
    }

    // join all threads at the end

    user_input_thread.join();

    // you might want to create similar communication with the UDP thread using another queue

    return 0;
}

Для очереди вы можете использовать существующее решение. Вот один, который я недавно использовал для связи 1-1 потока. GitHub Link Он лицензирован по упрощенной лицензии BSD, поэтому я думаю, что у вас все будет хорошо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...