QTcpSocket или QSslSocket автоматически создают поток для чтения/записи?

Несмотря на то, что нигде не используется std::thread или QThread, по-прежнему возникают следующие проблемы:

  1. Всегда журнал ошибок отладки во время выполнения из Qt:
    #P2#
  2. Периодический сбой при методе TcpSocket::flush();
    я использую этот метод, чтобы убедиться, что TCP записывается немедленно; Теперь иногда приложение вылетает именно по этому методу с SIGPIPE

При поиске в Интернете обнаружил, что люди предполагают, что для устранения 1-й проблемы (то есть мета-ошибки) мне нужно зарегистрироваться, используя qRegisterMetaType(), когда у нас есть несколько потоков.
Та же многопоточность также упоминается как причина для 2-й проблемы; см. это и это.

Но у меня не более 1 потока!
Код моего сокета выглядит следующим образом:

struct Socket : public QSslSocket
{
  Q_OBJECT public:

  void ConnectSlots ()
  {
    const auto connectionType = Qt::QueuedConnection;
    connect(this, SIGNAL(readyRead()), this, SLOT(ReceiveData()), connectionType);
    connect(this, SIGNAL(disconnected()), this, SLOT(Disconnected()), connectionType);
    connect(this, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(Error(QAbstractSocket::SocketError)), connectionType);
    //                           ^^^^^^^ error comes whether I comment this or not
  }

  public slots:
  void ReceiveData () { ... }
  void Disconnected () { ... }
  void Error () { ... }
}

Вопрос: создает ли Qt какой-либо внутренний поток для чтения/записи? (Надеюсь нет). Как исправить выше 2 проблемы?


person iammilind    schedule 13.07.2017    source источник
comment
Первое, что пришло мне в голову, это использовать int вместо типа enum в соединении сигнал/слот. т.е. ...SLOT(Error(int)..., если вы не хотите заморачиваться с метасистемой Qt и правильно приводить параметры к значениям перечисления в вашем слоте.   -  person vahancho    schedule 13.07.2017
comment
У вас есть комментарий в показанном коде, ошибка возникает независимо от того, комментирую я это или нет. Закомментируйте, что именно — только параметр QAbstractSocket::SocketError для Error или весь вызов для connect?   -  person G.M.    schedule 13.07.2017
comment
@vahancho, если я сделаю это Error(int), то возникнет ошибка несовместимого типа: QObject::connect: Несовместимые аргументы отправителя/получателя. Connection::Socket::error(QAbstractSocket::SocketError) --› Connection::Socket::Error(int). Предположим, что если я удалю аргумент и сделаю его Error(), то фактическая проблема, упомянутая в Qn, все еще сохраняется.   -  person iammilind    schedule 13.07.2017
comment
Зачем вам QueuedConnection? Обычно я использую AutoConnection по умолчанию   -  person Jeka    schedule 13.07.2017
comment
@Jeka, потому что много раз, когда вызывается функция Write (), до ее завершения вызывается Read () между ними. Логика моего кода предполагает, что Write() должна быть полностью завершена до начала Read() и наоборот. С QueuedConnection эта часть была решена.   -  person iammilind    schedule 13.07.2017
comment
@iammilind, подождите секунду, если ваш слот ReceiveData вызывается много раз, это означает, что в очереди основного потока будет много событий Receive, я не вижу ничего, что мешало бы помещать между ними события Write.   -  person Jeka    schedule 13.07.2017
comment
@Jeka, на самом деле я вообще не использую темы. Оба чтения/записи происходят в одном и том же потоке (или, по крайней мере, я так считаю). Я ожидаю, что когда слоты Write() выполняются, слот Read() не должен прерываться между ними и наоборот. т. е. позволить функции Read() или Write() завершить свое выполнение и позволить элементу управления вернуться в цикл обработки событий. Только после этого следует вызывать последующие сигналы/слоты.   -  person iammilind    schedule 13.07.2017
comment
@iammilind да, поскольку вы находитесь только в одном потоке, и нет никаких прерываний, доступных для любых вызовов слотов, вызванных только системными событиями, ожидающими в QEventLoop основного потока. В таком случае невозможно, чтобы чтение и запись могли быть прерваны. Какая у вас ОС? У вас есть пример, когда чтение или запись прерваны, я уверен, что это невозможно   -  person Jeka    schedule 13.07.2017


Ответы (2)


Я не думаю, что проблема связана с потоками, скорее проблема связана с комбинацией параметра типа QAbstractSocket::SocketError и Qt::QueuedConnection.

Глядя на различные реализации connect в исходном коде Qt5.8, если Qt::QueuedConnection указано в качестве типа соединения, то будет выполнена проверка на соответствие типам параметров сигнала. Что-то типа...

int *types = 0;
if ((type == Qt::QueuedConnection)
        && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) {
   return QMetaObject::Connection(0);
}

где queuedConnectionTypes вернет нулевой указатель, если какой-либо из типов не зарегистрирован.

Таким образом, если соединение поставлено в очередь, то все параметры, используемые сигналом, должны быть зарегистрированы независимо от того, используются ли они слотом или нет. Чтобы избежать ошибки, убедитесь, что вы звоните...

qRegisterMetaType<QAbstractSocket::SocketError>();

один раз в какой-то момент перед любыми вызовами connect, которые используют комбинацию параметра QAbstractSocket::SocketError и Qt::QueuedConnection.

person G.M.    schedule 13.07.2017
comment
Спасибо за ответ. Не могли бы вы также добавить некоторый код о том, как и где/когда сделать эту регистрацию (ради новичков). Кроме того, есть идеи о проблеме 2? Я предполагаю, что 1 и 2 могут быть связаны. Возможно, error() не сигнализируется из-за вышеуказанной проблемы --> слот Error() не вызывается --> сокет все еще продолжает писать и очищать отключенный сокет --> сбой. - person iammilind; 13.07.2017
comment
Отредактировал ответ, чтобы включить основное исправление - вызов qRegisterMetaType. Что касается проблемы flush, я пока не уверен. Они могут быть связаны, как вы говорите - SIGPIPE предполагает, что flush может вызывать запись после отключения/закрытия другого конца. - person G.M.; 13.07.2017
comment
Да, приведенное выше решение устраняет первую проблему. Вторая проблема, я должен ждать, чтобы воспроизвести (надеюсь, что это не так). Кстати, когда я смотрю на код qRegisterMetaType(), его определение имеет префикс QT_DEPRECATED. Я использую последнюю версию Qt 5.9. Это правильно? Нам случайно не нужно использовать Q_DECLARE_METATYPE()? Пожалуйста, объясните и это. - person iammilind; 13.07.2017
comment
Что касается макроса QT_DEPRECATED и qRegisterMetaType, я не уверен, почему. В документации не упоминается, что он объявлен устаревшим. Что касается Q_DECLARE_METATYPE, это уже используется для QAbstractSocket::SocketError в заголовках Qt, поэтому нет необходимости использовать его явно в собственном коде. - person G.M.; 13.07.2017

Нет, сокеты не создают отдельный поток для чтения/записи. Вместо этого ОС вызывает событие для данного дескриптора сокета всякий раз, когда наблюдается чтение/запись. Это событие должно быть поставлено в очередь. Следовательно, для этого Qt::QueuedConnection предпочтительнее.

QAbstractSocket::SocketError импровизирован и кажется специфичным для ОС. Этого нельзя избежать. Максимум, при возникновении такой ошибки сокет может быть уничтожен.

Чтобы избежать сбоев, всякий раз, когда происходит отключение в сокете, можно сделать следующее:

void Destroy (QWebSocket* const pSocket)
{
  if(pSocket == nullptr)
    return;
  pSocket->disconnect();  // no further signal/slot
  pSocket->close();  // graceful closure
  pSocket->deleteLater(); // don't delete immediately; let the Qt take care
  pSocket = nullptr; // to avoid further undefined behaviour
}

Даже после выполнения описанного выше иногда происходит сбой сокета из-за операции write(). а именно Когда сокет close()-es, он пытается flush() записать все данные. В это время, если удаленное соединение уже закрыто, ОС завершает работу программы с помощью события SIGPIPE. К сожалению, это невозможно предотвратить в C++ с помощью std::exceptions.

Решения, упомянутые в сообщении ниже, не помогают:
Как предотвращать SIGPIPE (или обрабатывать их должным образом)
Этого можно избежать, выполнив следующие действия:

if(pSocket->isValid())
  pSocket->sendBinaryMessage(QByteArray(...));

Таким образом, isValid() помогает в ситуации, когда сокет пытается что-то записать в уже отключенное удаленное соединение сокета.

person iammilind    schedule 19.04.2019
comment
This can be avoided by following: .... К сожалению, этого НЕЛЬЗЯ избежать таким образом, потому что сообщаемое состояние сокета (полученное путем вызова методов isValid() или state()) во время вызова flush в порядке, даже если удаленное соединение уже закрыто. Это единственная внутренняя реализация Qt, способная решить проблему. Qt ошибка? Тем временем я считаю, что метод flush() не работает. - person Artem Pisarenko; 02.10.2020
comment
Реализация Qt должна вызывать send() с флагом MSG_NOSIGNAL. - person Artem Pisarenko; 02.10.2020
comment
@ArtemPisarenko, с большим количеством свидетельств, вы можете подумать о том, чтобы сообщить об ошибке команде Qt. Я потерял след этой проблемы, как сегодня. Но не забудьте опубликовать этот ответ после того, как он устранил проблему в моем коде. - person iammilind; 02.10.2020
comment
Я только что обнаружил, что ошибался (частично). На самом деле, реализация Qt делает это правильно. Это была просто ошибка в моем приложении, из-за которой я подумал, что SIGPIPE вызвал сбой. Нет, это не так. - person Artem Pisarenko; 03.10.2020
comment
В моем случае источником путаницы был тот факт, что отладчик ловит SIGPIPE, приостанавливает выполнение, и моя ошибка срабатывает сразу после продолжения выполнения (заканчивающегося сбоем). Нужно просто настроить отладчик на игнорирование SIGPIPE. Так что это не приносит никакого вреда. Но вы все равно можете поймать его, независимо от того, выполняете ли вы проверку isValid() или нет. Кроме того, вызов flush() в этом случае будет выдавать сигнал stateChanged, так что будьте осторожны в связанных с ним слотах (по крайней мере, это было неожиданно для меня и привело к упомянутой мной ошибке). - person Artem Pisarenko; 03.10.2020