diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
index adca7470dae..e210b143535 100644
--- a/doc/CMakeLists.txt
+++ b/doc/CMakeLists.txt
@@ -238,6 +238,7 @@ if(JKQtPlotter_BUILD_EXAMPLES)
datastore_regression/datastore_regression,datastore_regression_lin,datastore_regression_linrobust,datastore_regression_linrobust_p,datastore_regression_linweight,datastore_regression_nonlinreg_exp,datastore_regression_nonlinreg_pow,datastore_regression_polynom,datastore_regression_polynom_errros/--iteratefunctorsteps
datastore_statistics/datastore_statistics,datastore_statistics_dataonly,datastore_statistics_boxplots_simple,datastore_statistics_boxplots_outliers,datastore_statistics_hist,datastore_statistics_kde,datastore_statistics_cumhistkde/--iteratefunctorsteps
datastore_statistics_2d/datastore_statistics_2d
+ multithreaded/multithreaded/--mdfile=${CMAKE_CURRENT_LIST_DIR}/../examples/multithreaded/README.md
)
@@ -279,7 +280,7 @@ if(JKQtPlotter_BUILD_EXAMPLES)
foreach(ex ${JKQTPlotter_GenerateDocScreenshots_From})
set(example ${ex})
set(basename ${ex})
- string(REGEX MATCH "(.+)/(.+)" dummy ${ex})
+ string(REGEX MATCH "([^/]+)/([^/]+)" dummy ${ex})
set(extra_command "")
if(CMAKE_MATCH_1 STREQUAL "" OR CMAKE_MATCH_2 STREQUAL "")
set(example ${ex})
@@ -288,7 +289,7 @@ if(JKQtPlotter_BUILD_EXAMPLES)
set(example ${CMAKE_MATCH_1})
set(basename ${CMAKE_MATCH_2})
set(CMAKE_MATCH_3 "")
- string(REGEX MATCH "(.+)/(.*)/(.+)" dummy ${ex})
+ string(REGEX MATCH "([^/]+)/([^/]*)/(.+)" dummy ${ex})
if(NOT (CMAKE_MATCH_3 STREQUAL ""))
set(example ${CMAKE_MATCH_1})
set(basename ${CMAKE_MATCH_2})
diff --git a/doc/dox/examples_and_tutorials.dox b/doc/dox/examples_and_tutorials.dox
index 65df90c5b2c..094f8b74596 100644
--- a/doc/dox/examples_and_tutorials.dox
+++ b/doc/dox/examples_and_tutorials.dox
@@ -127,7 +127,7 @@ All test-projects are Qt-projects that use qmake to build. You can load them int
\image html advancedlineandfillstyling_small.png
| \subpage JKQTPlotterAdvancedLineAndFillStyling
| `JKQTPXYLineGraph`, `JKQTPSpecialLineHorizontalGraph` and `JKQTPBarVerticalGraph` C++ vector of data advanced line styling and filling
@@ -252,6 +252,15 @@ All test-projects are Qt-projects that use qmake to build. You can load them int
| Allows to zoom into the Mandelbrot Set, using the different Zooming methods of JKQTPlotter
+\subsection jkqtp_extut_specialusecasesexamples Examples for special Use-Cases
+
+
+ Screenshot | Description | Notes
+ |
---|
\image html multithreaded_small.png
+ | \subpage JKQTPlotterMultiThreaded
+ | multi-threaded plotting using JKQTBasePlotter
+ |
+
\subsection jkqtp_extut_cmake_build Examples for CMake Build System
diff --git a/doc/dox/jkqtplotter_usage.dox b/doc/dox/jkqtplotter_usage.dox
index 2a72265999f..27f05a6fcc4 100644
--- a/doc/dox/jkqtplotter_usage.dox
+++ b/doc/dox/jkqtplotter_usage.dox
@@ -47,7 +47,11 @@
With simlar code you can also integrate JKQTBasePlotter into your own widgets.
-
+ This class is immpleented in a such a way that ^different instances can be used in different parallel threads, i.e. the class is re-entrant.
+ There are however access to different cached data is synchronized between all threads (i.e. static internal caches are used), which limmits
+ the acheavable parallelization speedup!
+
+ \see See \ref JKQTPlotterMultiThreaded for an example of multi-threaded plotting!
diff --git a/doc/dox/whatsnew.dox b/doc/dox/whatsnew.dox
index 4212ac00a74..5142fbf3e63 100644
--- a/doc/dox/whatsnew.dox
+++ b/doc/dox/whatsnew.dox
@@ -47,6 +47,7 @@ Changes, compared to \ref page_whatsnew_V4_0_0 "v4.0.0" include:
FIXED issue #99: Clipping of Tick Labels: for horizontal axes, additional space at the left and/or right of the plot is allowed so labels are no longer clipped (thanks to user:allenbarnett5/a> for reporting)
FIXED issue #99: Height of one-column key/legend was too large (thanks to user:allenbarnett5/a> for reporting)
FIXED issue mentioned in #110: Lock the panning action to certain values: View zooms in, when panning close to AbosluteXY (thanks to user:sim186 for reporting)
+ FIXED: jkqtpstatSum() and jkqtpstatSumSqr() did not work, as a non-existing function is called internally
FIXED/IMPROVED issue #100: Add option to disable resize delay feature by setting the delay to zero (thanks to user:fpalazzolo for reporting)
FIXED/NEW: placement of plot-title (was not centerd in its box, but glued to the bottom) by adding a plotstyle parameter JKQTBasePlotterStyle::plotLabelOffset
FIXED/REWORKED issue #111: Can't write to PDF files with JKQTPlotter::saveImage() when passing a filename ending in ".pdf" (thanks to user:fpalazzolo/a> for reporting): While fixing this issue, the functions JKQTBasePlotter::saveImage() etc. gained a bool return value to indicate whether sacing was successful.
@@ -108,6 +109,7 @@ Changes, compared to \ref page_whatsnew_V4_0_0 "v4.0.0" include:
NEW: added JKQTBasePlotterStyle::plotLabelTopBorder to set the spacing between top and plot label
NEW: Due to addition of JKQTMathText::setFontOptions() and the matchign extension of JKQTMathText::setFontSpecial() (see below) you can now add modifiers like +BOLD+ITALIC to any font-name provided to JKQTPlotter and in style INI-files
NEW: added JKQTPLabelMinBesides and JKQTPLabelMaxBesides to JKQTPLabelPosition, so labels can be set besides the axes
+ NEW/REWORKED JKQTBasePlottercan be used is re-entrant, i.e. different instances can be used from different threads in parallel (although there is significant overhead due to shared caches between the threads!). This is demonstrated and discussed in \ref JKQTPlotterMultiThreaded .
JKQTMathText:
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 46682cd620c..7459474707b 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -80,6 +80,7 @@ add_subdirectory(impulsesplot)
add_subdirectory(logaxes)
add_subdirectory(mandelbrot)
add_subdirectory(multiplot)
+add_subdirectory(multithreaded)
add_subdirectory(parametriccurve)
add_subdirectory(paramscatterplot)
add_subdirectory(paramscatterplot_image)
diff --git a/examples/multithreaded/CMakeLists.txt b/examples/multithreaded/CMakeLists.txt
new file mode 100644
index 00000000000..66418655b46
--- /dev/null
+++ b/examples/multithreaded/CMakeLists.txt
@@ -0,0 +1,33 @@
+cmake_minimum_required(VERSION 3.16)
+
+set(EXAMPLE_NAME multithreaded)
+set(EXENAME jkqtptest_${EXAMPLE_NAME})
+
+message( STATUS ".. Building Example ${EXAMPLE_NAME}" )
+
+
+# Set up source files
+set(SOURCES multithreaded.cpp )
+set(HEADERS multithreaded_thread.h )
+set(RESOURCES )
+set(UIS )
+
+add_executable(${EXENAME} WIN32 ${SOURCES} ${HEADERS} ${RESOURCES} ${UIS})
+target_link_libraries(${EXENAME} JKQTPExampleToolsLib)
+target_include_directories(${EXENAME} PRIVATE ../../lib)
+if(JKQtPlotter_BUILD_STATIC_LIBS)
+ target_link_libraries(${EXENAME} JKQTPlotterLib)
+
+elseif(JKQtPlotter_BUILD_SHARED_LIBS)
+ target_link_libraries(${EXENAME} JKQTPlotterSharedLib)
+endif()
+
+# precomiled headers to speed up compilation
+target_precompile_headers(${EXENAME} PRIVATE ../../lib/jkqtplotter/private/jkqtplotter_precomp.h)
+
+
+# Installation
+install(TARGETS ${EXENAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+#Installation of Qt DLLs on Windows
+jkqtplotter_deployqt(${EXENAME})
diff --git a/examples/multithreaded/README.md b/examples/multithreaded/README.md
new file mode 100644
index 00000000000..3d2187b2b42
--- /dev/null
+++ b/examples/multithreaded/README.md
@@ -0,0 +1,110 @@
+# Example (JKQTPlotter): Multi-Threaded (Parallel) Plotting {#JKQTPlotterMultiThreaded}
+This project (see `./examples/multithreaded/`) shows how to use JKQTBasePlotter in multiple threads in parallel.
+
+The source code of the main application can be found in [`multithreaded.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded.cpp) and [`multithreaded_thread.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded_thread.cpp).
+
+The file [`multithreaded_thread.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded_thread.cpp) contains a [`QThread`](https://doc.qt.io/qt-6/qthread.html) class that implements the actual plotting within a static method that is also run inside the thread's [`QThread::run()`](https://doc.qt.io/qt-6/qthread.html#run) method. It generates a plot with several line-graphs and then saves them into a PNG-file:
+
+```.cpp
+public:
+ inline static QString plotAndSave(const QString& filenamepart, int plotIndex, int NUM_GRAPHS, int NUM_DATAPOINTS, double* runtimeNanoseconds=nullptr) {
+ QElapsedTimer timer;
+ timer.start();
+ const QString filename=QDir(QDir::tempPath()).absoluteFilePath(QString("testimg_%1_%2.png").arg(filenamepart).arg(plotIndex));
+ JKQTBasePlotter plot(true);
+
+ const size_t colX=plot.getDatastore()->addLinearColumn(NUM_DATAPOINTS, 0, 10, "x");
+ QRandomGenerator rng;
+ for (int i=0; isetXColumn(colX);
+ g->setYColumn(plot.getDatastore()->addColumnCalculatedFromColumn(colX, [&](double x) { return cos(x+double(i)/8.0*JKQTPSTATISTICS_PI)+rng.generateDouble()*0.2-0.1;}));
+ g->setTitle(QString("Plot %1: $f(x)=\\cos\\leftx+\\frac{%1\\pi}{8}\\right)").arg(i+1));
+ g->setDrawLine(true);
+ g->setSymbolType(JKQTPNoSymbol);
+
+ }
+ plot.setPlotLabel(QString("Test Plot %1").arg(plotIndex+1));
+ plot.getXAxis()->setAxisLabel("x-axis");
+ plot.getYAxis()->setAxisLabel("y-axis");
+ plot.zoomToFit();
+ plot.saveAsPixelImage(filename, false, "PNG");
+
+ if (runtimeNanoseconds) *runtimeNanoseconds=timer.nsecsElapsed();
+ return filename;
+ }
+
+ // ...
+
+protected:
+ inline virtual void run() {
+ m_filename=plotAndSave(m_filenamepart, m_plotindex, m_NUM_GRAPHS, m_NUM_DATAPOINTS, &m_runtimeNanoseconds);
+ }
+```
+
+The main application in [`multithreaded.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded.cpp) then uses this method/thread-class to perform a test: First the function is run several times serially and then an equal amount of times in parallel.
+
+```.cpp
+ #define NUM_PLOTS 8
+ #define NUM_GRAPHS 6
+ #define NUM_DATAPOINTS 1000
+
+ QElapsedTimer timer;
+
+ /////////////////////////////////////////////////////////////////////////////////
+ // serial plotting
+ /////////////////////////////////////////////////////////////////////////////////
+ timer.start();
+ for (int i=0; i> threads;
+ for (int i=0; i::create("parallel",i, NUM_GRAPHS, NUM_DATAPOINTS, nullptr));
+ }
+ timer.start();
+ for (int i=0; istart();
+ }
+ for (int i=0; iwait();
+ }
+ const double durParallelNano=timer.nsecsElapsed();
+ qDebug()<<"durParallel = "<SERIAL RESULTS: runtime, overall = 1719.3ms single runtimes = (214.8 +/- 277.4) ms speedup = 1.00x threads / available = 1 / 16
+
+PARALLEL RESULTS:
+runtime, overall = 649.1ms single runtimes = (605.2 +/- 81.8) ms speedup = 7.46x threads / available = 8 / 16
speedup vs. serial = 2.6x
+
+[comment]:RESULTS_END
+
+From this data you can observe:
+ - The plotting parallelizes nicely, i.e. the speedup ist >7x on a 8-core-machine. This is the speedup calculated as sum of runtimes of each thread, divided by the runtime of all threads in parallel.
+ - BUT: the speedup of serialized plotting vs. parallel plotting is way smaller: It is only 2-3x. This can be explained by the (significant) overhead due to shared caches (and therefore synchronization) between the plotters. This may be reworked in future!
+ - The variance in runtimes in the (initial) serial test-run is larger than in the parallel run. This is due to filling of the internal caches during the first plotting!
+.
+
+Finally the application displays the plots:
+
+![multithreaded](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/multithreaded.png)
+
diff --git a/examples/multithreaded/multithreaded.cpp b/examples/multithreaded/multithreaded.cpp
new file mode 100644
index 00000000000..026c80a9e81
--- /dev/null
+++ b/examples/multithreaded/multithreaded.cpp
@@ -0,0 +1,152 @@
+/** \example multithreaded.cpp
+ * Using JKQTBasePlotter in multiple threads in parallel.
+ *
+ * \ref JKQTPlotterMultiThreaded
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "multithreaded_thread.h"
+#include "jkqtmath/jkqtpstatbasics.h"
+#include "jkqtpexampleapplication.h"
+
+#define NUM_SHOWN_PLOTS 3
+#define NUM_PLOTS 8
+#define NUM_GRAPHS 6
+#define NUM_DATAPOINTS 1000
+
+
+int main(int argc, char* argv[])
+{
+ JKQTPAppSettingController highDPIController(argc,argv);
+ JKQTPExampleApplication app(argc, argv);
+ QMainWindow* mainWin=new QMainWindow();
+ mainWin->setWindowTitle("Multi-Threaded Plotting");
+ QWidget* main;
+ mainWin->setCentralWidget(main=new QWidget(mainWin));
+
+ QString markdownFile="";
+ for (int i=1; isetLayout(mainLayout);
+ QVBoxLayout* lay_serial=new QVBoxLayout();
+ QVBoxLayout* lay_parallel=new QVBoxLayout();
+ mainLayout->addLayout(lay_serial);
+ QLabel* l;
+ lay_serial->addWidget(l=new QLabel("Serialized Plotting"));
+ QFont f=l->font();
+ f.setBold(true);
+ f.setPointSize(16);
+ l->setFont(f);
+ lay_parallel->addWidget(l=new QLabel("Parallel Plotting"));
+ l->setFont(f);
+
+ QLabel* labSerialResult=new QLabel(main);
+ lay_serial->addWidget(labSerialResult);
+ QLabel* labParallelResult=new QLabel(main);
+ lay_parallel->addWidget(labParallelResult);
+
+ mainLayout->addLayout(lay_parallel);
+ QVector pic_parallel, pic_serial;
+ for (int i=0; iaddWidget(pic_serial.last(), 1);
+ lay_parallel->addWidget(pic_parallel.last(), 1);
+ }
+
+ QElapsedTimer timer;
+ QList runtimesSerial;
+ timer.start();
+ for (int i=0; i runtimesParallel;
+ QList> threads;
+ for (int i=0; i::create("parallel",i, NUM_GRAPHS, NUM_DATAPOINTS, nullptr));
+ }
+ timer.start();
+ for (int i=0; istart();
+ }
+ for (int i=0; iwait()) {
+ filenamesParallel<getFilename();
+ runtimesParallel<getRuntimeNanosends()/1e6;
+ }
+ }
+ const double durParallelNano=timer.nsecsElapsed();
+ qDebug()<<"durParallel = "<NUM_SHOWN_PLOTS/2) i=NUM_PLOTS-1-NUM_SHOWN_PLOTS+ii;
+ pic_serial[ii]->setPixmap(QPixmap(filenamesSerial[i], "PNG"));
+ pic_parallel[ii]->setPixmap(QPixmap(filenamesParallel[i], "PNG"));
+ }
+ QString ser_result, par_result;
+ labSerialResult->setText(ser_result=QString("runtime, overall = %1ms single runtimes = (%2 +/- %3) ms speedup = %4x threads / available = %5 / %6
").arg(durSerialNano/1e6,0,'f',1).arg(jkqtpstatAverage(runtimesSerial.begin(), runtimesSerial.end()),0,'f',1).arg(jkqtpstatStdDev(runtimesSerial.begin(), runtimesSerial.end()),0,'f',1).arg(jkqtpstatSum(runtimesSerial.begin(), runtimesSerial.end())/(durSerialNano/1e6),0,'f',2).arg(1).arg(std::thread::hardware_concurrency()));
+ labParallelResult->setText(par_result=QString("runtime, overall = %1ms single runtimes = (%2 +/- %3) ms speedup = %4x threads / available = %5 / %6
speedup vs. serial = %7x").arg(durParallelNano/1e6,0,'f',1).arg(jkqtpstatAverage(runtimesParallel.begin(), runtimesParallel.end()),0,'f',1).arg(jkqtpstatStdDev(runtimesParallel.begin(), runtimesParallel.end()),0,'f',1).arg(jkqtpstatSum(runtimesParallel.begin(), runtimesParallel.end())/(durParallelNano/1e6),0,'f',2).arg(NUM_PLOTS).arg(std::thread::hardware_concurrency()).arg(durSerialNano/durParallelNano,0,'f',1));
+ mainWin->show();
+
+ if (!markdownFile.isEmpty()) {
+ qDebug()<<"modifying MD-file "<SERIAL RESULTS: "+ser_result.toUtf8()
+ +"\n\nPARALLEL RESULTS: \n"+par_result.toUtf8()+"\n\n";
+ md.replace(istart,iend-istart,newResults);
+ if (f.open(QFile::WriteOnly)) {
+ qDebug()<<" writing "<
+#include "jkqtplotter/jkqtpbaseplotter.h"
+#include "jkqtplotter/graphs/jkqtplines.h"
+#include "jkqtcommon/jkqtpmathtools.h"
+#include
+#include
+#include
+#include
+#include
+
+class PlottingThread: public QThread {
+ Q_OBJECT
+public:
+ inline PlottingThread(const QString& filenamepart, int plotindex, int NUM_GRAPHS, int NUM_DATAPOINTS, QObject* parent):
+ QThread(parent),
+ m_plotindex(plotindex),
+ m_runtimeNanoseconds(0),
+ m_filenamepart(filenamepart),
+ m_filename(),
+ m_NUM_GRAPHS(NUM_GRAPHS),
+ m_NUM_DATAPOINTS(NUM_DATAPOINTS)
+ {}
+
+ inline static QString plotAndSave(const QString& filenamepart, int plotIndex, int NUM_GRAPHS, int NUM_DATAPOINTS, double* runtimeNanoseconds=nullptr) {
+ QElapsedTimer timer;
+ timer.start();
+ const QString filename=QDir(QDir::tempPath()).absoluteFilePath(QString("testimg_%1_%2.png").arg(filenamepart).arg(plotIndex));
+ JKQTBasePlotter plot(true);
+
+ const size_t colX=plot.getDatastore()->addLinearColumn(NUM_DATAPOINTS, 0, 10, "x");
+ QRandomGenerator rng;
+ for (int i=0; isetXColumn(colX);
+ g->setYColumn(plot.getDatastore()->addColumnCalculatedFromColumn(colX, [&](double x) { return cos(x+double(i)/8.0*JKQTPSTATISTICS_PI)+rng.generateDouble()*0.2-0.1;}));
+ g->setTitle(QString("Plot %1: $f(x)=\\cos\\leftx+\\frac{%1\\pi}{8}\\right)").arg(i+1));
+ g->setDrawLine(true);
+ g->setSymbolType(JKQTPNoSymbol);
+
+ }
+ plot.setPlotLabel(QString("Test Plot %1").arg(plotIndex+1));
+ plot.getXAxis()->setAxisLabel("x-axis");
+ plot.getYAxis()->setAxisLabel("y-axis");
+ plot.zoomToFit();
+ plot.saveAsPixelImage(filename, false, "PNG");
+
+ if (runtimeNanoseconds) *runtimeNanoseconds=timer.nsecsElapsed();
+ return filename;
+ }
+ inline double getRuntimeNanosends() const {
+ return m_runtimeNanoseconds;
+ }
+ inline QString getFilename() const {
+ return m_filename;
+ }
+protected:
+ inline virtual void run() {
+ m_filename=plotAndSave(m_filenamepart, m_plotindex, m_NUM_GRAPHS, m_NUM_DATAPOINTS, &m_runtimeNanoseconds);
+ }
+
+ int m_plotindex;
+ double m_runtimeNanoseconds;
+ QString m_filename;
+ QString m_filenamepart;
+ const int m_NUM_GRAPHS;
+ const int m_NUM_DATAPOINTS;
+};
+
+#endif // MULTITHREADED_THREAD_H
diff --git a/screenshots/multithreaded.png b/screenshots/multithreaded.png
new file mode 100644
index 00000000000..f2c6397ffa2
Binary files /dev/null and b/screenshots/multithreaded.png differ
diff --git a/screenshots/multithreaded_small.png b/screenshots/multithreaded_small.png
new file mode 100644
index 00000000000..8ebadff6dfb
Binary files /dev/null and b/screenshots/multithreaded_small.png differ
|