Предотвращение блокирования событий при использовании libfcgi в многопоточном режиме
Для создания приложений FastCGI на C/C++ есть библиотека libfcgi. Не буду вдаваться в дискуссию, зачем нужны приложения FastCGI на C/C++/подставить нужный язык, когда Python/PHP/Perl/подставить нужное гораздо удобнее. Отмечу лишь, что по работе понадобилось написать FastCGI-приложение на Qt (в основном из-за наличия нескольких высокопроизводительных библиотек, написанных на Qt, но не суть).
Строго говоря, libfcgi, хотя и является официальной библиотекой от создателей протокола, не лучший вариант для поддержки FastCGI — API, предоставляемое библиотекой, сильно ограничено (в плане функциональности) и недостаточно гибко.
Грубо говоря, приложение, использующее libfcgi, может работать в двух режимах:
- Последовательная обработка запросов: приложение получило запрос, обработало его, отправило результат, получило следующий запрос. Схема хороша, если обработка запроса тривиальна и/или не занимает много времени. Скорее всего, под большой нагрузкой масштабируется плохо, ибо чем длиннее очередь запросов, тем дольши придётся ждать клиенту.
- Многопоточная обработка запросов: приложение создаёт определённое количество потоков, каждый из которых будет обрабатывать свой запрос. При средних нагрузках эта схема работает лучше (если один поток занят, свободный поток займётся выполнением запроса). Но в предельном случае (когда все потоки заняты) имеем те же проблемы, что и в первом варианте. Схему можно немного изменить, создавая потоки по мере необходимости, но всё равно плодить потоки до бесконечности нельзя.
Использование потоков не всегда уместно/хорошо: многопоточные программы, как правило, сложнее для разработки и отладки; кроме того, потоки потребляют системные ресурсы (например, память для стека) и при большом количестве потоков эти издержки могут быть существенными (например, для Linux/IA-32 размер стека для потока по умолчанию составляет 2 мегабайта).
К сожалению, при использовании libfcgi отказаться от потоков очень трудно — дело в том, что библиотека использует блокирующие операции при работе с сокетами.
В общем случае алгоритм работы с libfcgi следующий:
FCGX_Init();
int socket = FCGX_OpenSocket(":9001", 256);
/* ... */
// Собственно работа
while (true) {
FCGX_Request* req = new FCGX_Request;
FCGX_InitRequest(req, socket, 0);
int rc = FCGX_Accept_r(req);
if (rc >= 0) {
/* Здесь создаём и запускаем поток, отвечающий за обработку запроса */
}
}
В случае с Qt и использованием рабочего цикла внутри основной программы имеется неприятный подводный камень: из-за того, что FCGX_Accept_r()
использует блокирующие вызовы при работе с сокетом, основная программа может очень долго не увидеть сообщения, посылаемые, например, потоком.
Поэтому перед вызовом FCGX_Accept_r()
имеет смысл проверять, имеются ли запросы на соединение.
Решение в лоб:
#include <sys/select.h>
/* ... */
int rc;
do {
QCoreApplication::processEvents();
struct timeval tv = { 0, 500 };
fd_set r;
FD_ZERO(&r);
FD_SET(socket, &r);
rc = ::select(socket+1, &r, 0, 0, &tv);
} while (!rc);
FCGX_Request* req = new FCGX_Request;
FCGX_InitRequest(req, socket, 0);
int sock = FCGX_Accept_r(req);
/* ... */
Но более корректно будет использовать QSocketNotifier
:
#include <QtCore/QObject>
class QSocketNotifier;
class Core : public QObject {
Q_OBJECT
public:
explicit Core(QObject *parent = 0);
void run(void);
private:
QSocketNotifier* m_notifier;
private slots:
void connectionPending(int socket);
};
// core.cpp
Core::Core(QObject *parent) : QObject(parent), m_notifier(0)
{
FCGX_Init();
int sock = FCGX_OpenSocket(":9001", 256);
this->m_notifier = new QSocketNotifier(sock, QSocketNotifier::Read);
QObject::connect(this->m_notifier, SIGNAL(activated(int)), this, SLOT(connectionPending(int)));
}
void Core::connectionPending(int socket)
{
QSocketNotifier* notifier = qobject_cast<QSocketNotifier*>(this->sender());
Q_CHECK_PTR(notifier);
notifier->setEnabled(false);
FCGX_Request* req = new FCGX_Request;
FCGX_InitRequest(req, socket, 0);
int s = FCGX_Accept_r(req);
if (s >= 0) {
/* Здесь создаём и запускаем поток */
}
notifier->setEnabled(true);
}
В качестве альтернативы созданию потоков можно предложить использовать очередь запросов.
#include <QtCore/QMutexLocker>
#include <QtCore/QQueue>
#include <QtCore/QWaitCondition>
class Queue {
public:
void addToQueue(FCGX_Request* r, bool broadcast = false)
{
QMutexLocker locker(&this->mx);
this->queue.enqueue(r);
if (broadcast) {
this->cv.wakeAll();
}
else {
this->cv.wakeOne();
}
}
FCGX_Request* getFromQueue(void)
{
this->mx.lock();
while (this->queue.isEmpty()) {
this->cv.wait(&this->mx);
}
FCGX_Request* result = this->queue.dequeue();
this->mx.unlock();
return result;
}
private:
QMutex mx;
QWaitCondition cv;
QQueue<FCGX_Request*> queue;
};
Но это уже совсем другая история.
© 2014 सत्यं वद धर्मं चर. Все права защищены. Перепубликация материалов без разрешения автора запрещена.
При использовании материалов блога наличие активной не закрытой от индексирования ссылки на источник обязательно.