3.8.2017
V posledním článků ze série paralelních výpočtů v Qt jsem rozvedl použití neblokujících operací a použití třídy QFuture. Neblokující operace jsou důležité, pokud aplikace pracuje v GUI aplikaci a chcete mít možnost výpočet ovládat nebo během výpočtu pracovat v jiných částech GUI.
Od minulého dílu uteklo poměrně hodně času, protože jsem se pustil do analýz dat z fotovoltaických elektráren pomocí neuronových sítí. S ohledem na množství dat je u této aplikace použití paralelního zpracování holou nutností, takže mám problematiku dnešního článku důkladně vyzkoušenou.
V předchozím díle jsem lehce nastínil použití třídy QFuture pro neblokující operace. Díky použití metody QFuture::waitForFinished() byla sice výsledná aplikace nakonec blokující, ale princip bylo možné z příkladu pochopit. Pokud chceme použít neblokující operace, musíme se postarat, aby vstupní množina dat nepřestala existovat s ukončením metody run(), stejně tak třída QFuture a QFutureWatcher musejí existovat po celou dobu výpočtu. Je proto potřeba uložit vše potřebné do příslušné třídy.
Třída QFutureWatcher disponuje sloty pro ovládání výpočtu a také signály pro hlášení o stavu výpočtu. Tyto signály a slot je nutné spojit s ovládacími prvky GUI (v konstruktoru třídy).
class Vypocet : public QWidget, private Ui::Vypocet { Q_OBJECT public: // Součástí GUI je tlačítko "Start", tlačítko "Pause" a tlačítko "Cancel" // Tlačítko "Pause" slouží jak k pozastavení, tak k opětovnému spuštění výpočtu // průběh výpočtu zobrazuje QProgressBar Vypocet(QWidget *parent); public slots: void run(); private slots: void slotFinished(); void slotPauseChanged(); private: QFuture<void> m_future; QFutureWatcher<void> m_watcher; QList<int> m_inputvalues; // Vstupní hodnoty pro výpočet void setEnabled(bool); static void vypocet(int); }; // Kontruktor, propojí tlačítka s paralelním výpočtem // a signály z paralelního výpočtu (třída QFutureWatcher) zpět do GUI Vypocet(QWidget *parent) : QWidget(parent) { setupUi(this); // propojení tlačítek s paralelním výpočtem connect(f_tlacitko_run, SIGNAL(clicked()), this, SLOT(run())); connect(f_tlacitko_cancel, SIGNAL(clicked()), &m_watcher, SLOT(cancel())); connect(f_tlacitko_pause, SIGNAL(clicked()), &m_watcher, SLOT(togglePause())); connect(&m_watcher, SIGNAL(progressValueChanged(int)), f_progressbar, SLOT(valueChanged(int))); connect(&m_watcher, SIGNAL(finished()), this, SLOT(slotFinished())); connect(&m_watcher, SIGNAL(paused()), this, SLOT(slotPauseChanged())); connect(&m_watcher, SIGNAL(resumed()), this, SLOT(slotPauseChanged())); } // Změní text tlačítka "Pause" na "Pause" nebo "Resume" při změně stavu výpočtu void Vypocet::slotPauseChanged() { f_tlacitko_pause->setText( (m_watcher.isPaused()) ? tr("Resume") : tr("Pause") ); } // Uklidí po dokončení výpočtu void Vypocet::slotFinished() { m_inputvalues.clear(); setEnabled(true); } // Povolí nebo zakáže tlačítka void Vypocet::setEnabled(bool enable) { f_tlacitko_run(enable); f_tlacitko_cancel(!enable); f_tlacitko_pause(!enable); f_progressbar(!enable); } // Spustí paralelní výpočet // Vstupní data se načítají z databáze (objekt DB – zde jen naznačeno) void run() { setEnabled(false); f_tlacitko_pause->setText( tr("Pause") ); m_inputvalues = DB->nactiVstupniHodnoty(); // načtení vstupních hodnot z databáze f_progressbar->setMaximum(m_inputvalues.size()); m_future = QtConcurrent::map(m_inputvalues, vypocet); m_watcher.setFuture(m_future); } // Samotný paralelní výpočet // Vstupní hodnotou je některá hodnota z pole m_inputvalues // Jde o statickou funkci, musí být reentrantní a použitelná ve více vláknech, tj. // pouze automatické promměnné, po ukončení musí po sobě uklidit. // Pokud se připojujete k databázi, je nutné mít připojení k DB v každém // vlákně samostatně. Nelze přistupovat do GUI, lze předávat signály. void vypocet(int x) { Database *db = new Database(); db->nactiData(); // samotný výpočet db->ulozData(); delete db; }
Zastavení paralelního výpočtu může v některých případech trvat delší dobu. Při zastavení výpočtu (ať už tlačítkem "Cancel", tak tlačítkem "Pause") se pouze přestanou plánovat nové výpočty. Výpočty, které již běží, se nechají doběhnout a ke kompletnímu zastavení tak dojde až po doběhnutí posledního běžícího vlákna. Pokud jeden výpočet trvá například minutu, dojde k zastavení paralelního výpočtu až po této době.
Problematické může být použití třídy QThreadStorage. Pro výpočet se sice používá stále stejný pool vláken, ta se však vytvářejí ještě před spuštěním paralelního výpočtu. Pokoušel jsem se použít tuto třídu pro zajištění persistentního připojení k databázi, ale nakonec jsem stejně použil pro každý samostatný výpočet nové připojení k databázi.
U databáze je nutné mít v každém vlákně samostatné připojení. Ze hry tak může vypadnout například databáze Sqlite, která je jednovláknová.
Pokud databázový server i paralelní výpočet běží na jednom stroji, na procesoru s osmi jádry není zrychlení osminásobné. Vhodnější je uložit databázi na samostatný stroj (s osmi a více jádry), potom se dá dosáhnout výraznější rychlosti.
Zpracování velkých objemů tvoří značnou část mé práce. Paralelní zpracování v Qt se používá jednoduše a představuje velké urychlení výpočtů na vícejádrových procesorech. Výpočet, který by jednovláknové aplikaci trval celý pracovní týden (v pondělí se spustí, v pátek jsou k dispozici výsledky), je možné na osmijádrovém stroji dokončit přes noc. K dispozici mám několik strojů s osmijádrovými AMD procesory FX8350. Uvedený procesor lze přitom dnes pořídit již za cenu kolem 3600 Kč.
Mimochodem – těším se na uvedení procesoru ThreadRipper na trh, u nejsilnější varianty bych mohl očekávat více než pětinásobné zrychlení oproti mému součanému procesoru.