summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2020-05-25 13:48:45 -0400
committerGitHub <noreply@github.com>2020-05-25 13:48:45 -0400
commita721347fd130fcab19f2709892093f087bfac19e (patch)
treeab4d6670668f519dd89066197cda349811f8978b
parent08ae9db2fd2a07acb2433275fc84c2ca47267a92 (diff)
parent9212a7d2e9611994dfc5fa2066b8ecbe68b198a8 (diff)
Merge pull request #1079 from Holt59/moinfo-improvements
ModInfo improvements
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/mainwindow.cpp4
-rw-r--r--src/modinfo.cpp12
-rw-r--r--src/modinfo.h36
-rw-r--r--src/modinfooverwrite.cpp1
-rw-r--r--src/modinforegular.cpp92
-rw-r--r--src/modinforegular.h7
-rw-r--r--src/modinfoseparator.h2
-rw-r--r--src/modinfowithconflictinfo.cpp33
-rw-r--r--src/modinfowithconflictinfo.h63
-rw-r--r--src/modlist.cpp4
-rw-r--r--src/modlist.h2
-rw-r--r--src/organizercore.cpp8
-rw-r--r--src/thread_utils.h103
14 files changed, 274 insertions, 94 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6625e4b6..f1f54cbc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -186,6 +186,7 @@ add_filter(NAME src/utilities GROUPS
shared/util
usvfsconnector
shared/windows_error
+ thread_utils
json
)
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index b5beb229..de4a6b39 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -3061,8 +3061,6 @@ void MainWindow::ignoreMissingData_clicked()
for (QModelIndex idx : selection->selectedRows()) {
int row_idx = idx.data(Qt::UserRole + 1).toInt();
ModInfo::Ptr info = ModInfo::getByIndex(row_idx);
- //QDir(info->absolutePath()).mkdir("textures");
- info->testValid();
info->markValidated(true);
connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
@@ -3070,8 +3068,6 @@ void MainWindow::ignoreMissingData_clicked()
}
} else {
ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
- //QDir(info->absolutePath()).mkdir("textures");
- info->testValid();
info->markValidated(true);
connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
diff --git a/src/modinfo.cpp b/src/modinfo.cpp
index 379fa071..15fa71da 100644
--- a/src/modinfo.cpp
+++ b/src/modinfo.cpp
@@ -29,6 +29,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "modinfodialog.h"
#include "overwriteinfodialog.h"
#include "versioninfo.h"
+#include "thread_utils.h"
#include <iplugingame.h>
#include <versioninfo.h>
@@ -249,6 +250,7 @@ void ModInfo::updateFromDisc(const QString &modDirectory,
DirectoryEntry **directoryStructure,
PluginContainer *pluginContainer,
bool displayForeign,
+ std::size_t refreshThreadCount,
MOBase::IPluginGame const *game)
{
TimeThis tt("ModInfo::updateFromDisc()");
@@ -285,8 +287,11 @@ void ModInfo::updateFromDisc(const QString &modDirectory,
createFromOverwrite(pluginContainer, game, directoryStructure);
std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName);
+
+ parallelMap(std::begin(s_Collection), std::end(s_Collection), &ModInfo::prefetch, refreshThreadCount);
updateIndices();
+
}
@@ -306,7 +311,7 @@ void ModInfo::updateIndices()
ModInfo::ModInfo(PluginContainer *pluginContainer)
- : m_Valid(false), m_PrimaryCategory(-1)
+ : m_PrimaryCategory(-1)
{
}
@@ -528,11 +533,6 @@ bool ModInfo::categorySet(int categoryID) const
return false;
}
-void ModInfo::testValid()
-{
- m_Valid = doTestValid();
-}
-
QUrl ModInfo::parseCustomURL() const
{
if (!hasCustomURL() || getCustomURL().isEmpty()) {
diff --git a/src/modinfo.h b/src/modinfo.h
index f582cc32..29e6124d 100644
--- a/src/modinfo.h
+++ b/src/modinfo.h
@@ -142,6 +142,7 @@ public:
MOShared::DirectoryEntry **directoryStructure,
PluginContainer *pluginContainer,
bool displayForeign,
+ std::size_t refreshThreadCount,
MOBase::IPluginGame const *game);
static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); }
@@ -702,7 +703,7 @@ public:
/**
* @return true if this mod is considered "valid", that is: it contains data used by the game
**/
- virtual bool isValid() const { return m_Valid; }
+ virtual bool isValid() const = 0;
/**
* @return true if the file has been endorsed on nexus
@@ -715,11 +716,6 @@ public:
virtual ETrackedState trackedState() const { return TRACKED_FALSE; }
/**
- * @brief updates the valid-flag for this mod
- */
- void testValid();
-
- /**
* @brief updates the mod to flag it as converted in order to ignore the alternate game warning
*/
virtual void markConverted(bool) {}
@@ -801,6 +797,13 @@ public:
**/
QUrl parseCustomURL() const;
+public slots:
+
+ /**
+ * @brief Notify this mod that the content of the disk may have changed.
+ */
+ virtual void diskContentModified() = 0;
+
signals:
/**
@@ -812,17 +815,22 @@ signals:
protected:
+ /**
+ *
+ */
ModInfo(PluginContainer *pluginContainer);
- static void updateIndices();
- static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS);
-
/**
- * @brief check if the content of this mod is valid.
+ * @brief Prefetch content for this mod.
*
- * @return true if the content is valid, false otherwize.
- **/
- virtual bool doTestValid() const = 0;
+ * This method can be used to prefetch content from the mod, e.g., for isValid()
+ * or getContents(). This method will only be called when first creating the mod
+ * using multiple threads for all the mods.
+ */
+ virtual void prefetch() = 0;
+
+ static void updateIndices();
+ static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS);
private:
@@ -848,8 +856,6 @@ private:
static std::map<std::pair<QString, int>, std::vector<unsigned int> > s_ModsByModID;
static int s_NextID;
- bool m_Valid;
-
};
diff --git a/src/modinfooverwrite.cpp b/src/modinfooverwrite.cpp
index 3b914134..8aad6209 100644
--- a/src/modinfooverwrite.cpp
+++ b/src/modinfooverwrite.cpp
@@ -9,7 +9,6 @@
ModInfoOverwrite::ModInfoOverwrite(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, MOShared::DirectoryEntry **directoryStructure)
: ModInfoWithConflictInfo(pluginContainer, game, directoryStructure)
{
- testValid();
}
bool ModInfoOverwrite::isEmpty() const
diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp
index d89716da..a1f1a04d 100644
--- a/src/modinforegular.cpp
+++ b/src/modinforegular.cpp
@@ -38,7 +38,6 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa
, m_TrackedState(TRACKED_UNKNOWN)
, m_NexusBridge(pluginContainer)
{
- testValid();
m_CreationTime = QFileInfo(path.absolutePath()).birthTime();
// read out the meta-file for information
readMeta();
@@ -654,61 +653,60 @@ std::vector<ModInfo::EFlag> ModInfoRegular::getFlags() const
}
-std::vector<ModInfo::EContent> ModInfoRegular::getContents() const
+std::vector<ModInfo::EContent> ModInfoRegular::doGetContents() const
{
- QTime now = QTime::currentTime();
- if (m_LastContentCheck.isNull() || (m_LastContentCheck.secsTo(now) > 60)) {
- m_CachedContent.clear();
- QDir dir(absolutePath());
- if (dir.entryList(QStringList() << "*.esp" << "*.esm" << "*.esl").size() > 0) {
- m_CachedContent.push_back(CONTENT_PLUGIN);
- }
- if (dir.entryList(QStringList() << "*.bsa" << "*.ba2").size() > 0) {
- m_CachedContent.push_back(CONTENT_BSA);
- }
- //use >1 for ini files since there is meta.ini in all mods already.
- if (dir.entryList(QStringList() << "*.ini").size() > 1) {
- m_CachedContent.push_back(CONTENT_INI);
- }
+ auto tree = contentFileTree();
+ std::vector<ModInfo::EContent> contents;
- if (dir.entryList(QStringList() << "*.modgroups").size() > 0) {
- m_CachedContent.push_back(CONTENT_MODGROUP);
+ for (auto e : *tree) {
+ if (e->isFile()) {
+ auto suffix = e->suffix().toLower();
+ if (suffix == "esp" || suffix == "esm" || suffix == "esl") {
+ contents.push_back(CONTENT_PLUGIN);
+ }
+ else if (suffix == "bsa" || suffix == "ba2") {
+ contents.push_back(CONTENT_BSA);
+ }
+ else if (suffix == "ini" && e->compare("meta.ini") != 0) {
+ contents.push_back(CONTENT_INI);
+ }
+ else if (suffix == "modgroups") {
+ contents.push_back(CONTENT_MODGROUP);
+ }
}
+ else {
+ if (e->compare("textures") == 0 || e->compare("icons") == 0 || e->compare("bookart") == 0)
+ contents.push_back(CONTENT_TEXTURE);
+ if (e->compare("meshes") == 0)
+ contents.push_back(CONTENT_MESH);
+ if (e->compare("interface") == 0 || e->compare("menus") == 0)
+ contents.push_back(CONTENT_INTERFACE);
+ if (e->compare("music") == 0 || e->compare("sound") == 0)
+ contents.push_back(CONTENT_SOUND);
+ if (e->compare("scripts") == 0)
+ contents.push_back(CONTENT_SCRIPT);
+ if (e->compare("SkyProc Patchers") == 0)
+ contents.push_back(CONTENT_SKYPROC);
+ if (e->compare("MCM") == 0)
+ contents.push_back(CONTENT_MCM);
+ }
+ }
- ScriptExtender *extender = qApp->property("managed_game")
- .value<IPluginGame *>()
- ->feature<ScriptExtender>();
-
- if (extender != nullptr) {
- QString sePluginPath = extender->PluginPath();
- if (dir.exists(sePluginPath)) {
- m_CachedContent.push_back(CONTENT_SKSEFILES);
- QDir sePluginDir(absolutePath() + "/" + sePluginPath);
- if (sePluginDir.entryList(QStringList() << "*.dll").size() > 0) {
- m_CachedContent.push_back(CONTENT_SKSE);
+ ScriptExtender* extender = m_GamePlugin->feature<ScriptExtender>();
+ if (extender != nullptr) {
+ auto e = tree->findDirectory(extender->PluginPath());
+ if (e) {
+ contents.push_back(CONTENT_SKSEFILES);
+ for (auto f : *e) {
+ if (f->hasSuffix("dll")) {
+ contents.push_back(CONTENT_SKSE);
+ break;
}
}
}
- if (dir.exists("textures") || dir.exists("icons") || dir.exists("bookart"))
- m_CachedContent.push_back(CONTENT_TEXTURE);
- if (dir.exists("meshes"))
- m_CachedContent.push_back(CONTENT_MESH);
- if (dir.exists("interface") || dir.exists("menus"))
- m_CachedContent.push_back(CONTENT_INTERFACE);
- if (dir.exists("music") || dir.exists("sound"))
- m_CachedContent.push_back(CONTENT_SOUND);
- if (dir.exists("scripts"))
- m_CachedContent.push_back(CONTENT_SCRIPT);
- if (dir.exists("SkyProc Patchers"))
- m_CachedContent.push_back(CONTENT_SKYPROC);
- if (dir.exists("MCM"))
- m_CachedContent.push_back(CONTENT_MCM);
-
- m_LastContentCheck = QTime::currentTime();
}
- return m_CachedContent;
-
+ return contents;
}
diff --git a/src/modinforegular.h b/src/modinforegular.h
index 0f22b6cb..e63e7570 100644
--- a/src/modinforegular.h
+++ b/src/modinforegular.h
@@ -289,8 +289,6 @@ public:
*/
virtual std::vector<EFlag> getFlags() const override;
- virtual std::vector<EContent> getContents() const override;
-
/**
* @return an indicator if and how this mod should be highlighted by the UI
*/
@@ -418,6 +416,8 @@ private slots:
protected:
+ virtual std::vector<EContent> doGetContents() const override;
+
ModInfoRegular(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &path, MOShared::DirectoryEntry **directoryStructure);
private:
@@ -462,9 +462,6 @@ private:
NexusBridge m_NexusBridge;
- mutable std::vector<ModInfo::EContent> m_CachedContent;
- mutable QTime m_LastContentCheck;
-
bool needsDescriptionUpdate() const;
};
diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h
index 4cfb9ea0..a7a6cb5f 100644
--- a/src/modinfoseparator.h
+++ b/src/modinfoseparator.h
@@ -49,7 +49,7 @@ public:
virtual void addInstalledFile(int /*modId*/, int /*fileId*/) override { }
protected:
- virtual bool doTestValid() const override { return true; }
+ virtual bool doIsValid() const override { return true; }
private:
diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp
index 0c67b4f7..b0edf581 100644
--- a/src/modinfowithconflictinfo.cpp
+++ b/src/modinfowithconflictinfo.cpp
@@ -14,7 +14,11 @@ namespace fs = std::filesystem;
ModInfoWithConflictInfo::ModInfoWithConflictInfo(
PluginContainer *pluginContainer, const MOBase::IPluginGame* gamePlugin, DirectoryEntry **directoryStructure)
- : ModInfo(pluginContainer), m_GamePlugin(gamePlugin), m_DirectoryStructure(directoryStructure), m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {}
+ : ModInfo(pluginContainer), m_GamePlugin(gamePlugin),
+ m_FileTree([this]() { return QDirFileTree::makeTree(absolutePath()); }),
+ m_Valid([this]() { return doIsValid(); }),
+ m_Contents([this]() { return doGetContents(); }),
+ m_DirectoryStructure(directoryStructure), m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {}
void ModInfoWithConflictInfo::clearCaches()
{
@@ -296,14 +300,37 @@ bool ModInfoWithConflictInfo::hasHiddenFiles() const
return m_HasHiddenFiles;
}
+void ModInfoWithConflictInfo::diskContentModified() {
+ m_FileTree.invalidate();
+ m_Valid.invalidate();
+ m_Contents.invalidate();
+}
+
+void ModInfoWithConflictInfo::prefetch() {
+ // Populating the tree to 1-depth (IFileTree is lazy, so size() forces the
+ // tree to populate the first level):
+ contentFileTree()->size();
+}
-bool ModInfoWithConflictInfo::doTestValid() const {
+bool ModInfoWithConflictInfo::doIsValid() const {
auto mdc = m_GamePlugin->feature<ModDataChecker>();
if (mdc) {
- auto qdirfiletree = QDirFileTree::makeTree(absolutePath());
+ auto qdirfiletree = contentFileTree();
return mdc->dataLooksValid(qdirfiletree);
}
return true;
}
+
+std::shared_ptr<const IFileTree> ModInfoWithConflictInfo::contentFileTree() const {
+ return m_FileTree.value();
+}
+
+bool ModInfoWithConflictInfo::isValid() const {
+ return m_Valid.value();
+}
+
+std::vector<ModInfo::EContent> ModInfoWithConflictInfo::getContents() const {
+ return m_Contents.value();
+}
diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h
index 94a61c62..abc9f223 100644
--- a/src/modinfowithconflictinfo.h
+++ b/src/modinfowithconflictinfo.h
@@ -1,6 +1,9 @@
#ifndef MODINFOWITHCONFLICTINFO_H
#define MODINFOWITHCONFLICTINFO_H
+#include <ifiletree.h>
+
+#include "thread_utils.h"
#include "modinfo.h"
#include <QTime>
@@ -14,6 +17,16 @@ public:
virtual std::vector<ModInfo::EFlag> getFlags() const override;
/**
+ * @return true if this mod is considered "valid", that is: it contains data used by the game
+ **/
+ virtual bool isValid() const override;
+
+ /**
+ * @return a list of content types contained in a mod
+ */
+ virtual std::vector<EContent> getContents() const override;
+
+ /**
* @brief clear all caches held for this mod
*/
virtual void clearCaches() override;
@@ -32,14 +45,39 @@ public:
virtual void doConflictCheck() const override;
+public slots:
+
+ /**
+ * @brief Notify this mod that the content of the disk may have changed.
+ */
+ virtual void diskContentModified();
+
protected:
/**
- * @brief check if the content of this mod is valid.
+ * @brief Check if the content of this mod is valid.
+ *
+ * @return true if the content is valid, false otherwise.
+ **/
+ virtual bool doIsValid() const;
+
+ /**
+ * @brief Compute the contents for this mod.
*
- * @return true if the content is valid, false otherwize.
+ * @return the contents for this mod.
**/
- virtual bool doTestValid() const;
+ virtual std::vector<EContent> doGetContents() const { return {}; }
+
+ /**
+ * @brief Retrieve a file tree corresponding to the underlying disk content
+ * of this mod.
+ *
+ * The file tree should not be cached since it is already cached and updated when
+ * required.
+ *
+ * @return a file tree representing the content of this mod.
+ */
+ std::shared_ptr<const MOBase::IFileTree> contentFileTree() const;
ModInfoWithConflictInfo(
PluginContainer* pluginContainer,
@@ -81,10 +119,25 @@ private:
bool hasHiddenFiles() const;
-private:
+protected:
+
+ /**
+ * @brief Prefetch content for this mod.
+ *
+ * This method can be used to prefetch content from the mod, e.g., for isValid()
+ * or getContents(). This method will only be called when first creating the mod
+ * using multiple threads for all the mods.
+ */
+ virtual void prefetch() override;
// Current game plugin running in MO2:
- MOBase::IPluginGame const* m_GamePlugin;
+ MOBase::IPluginGame const * const m_GamePlugin;
+
+private:
+
+ MOShared::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_FileTree;
+ MOShared::MemoizedLocked<bool> m_Valid;
+ MOShared::MemoizedLocked<std::vector<EContent>> m_Contents;
MOShared::DirectoryEntry **m_DirectoryStructure;
diff --git a/src/modlist.cpp b/src/modlist.cpp
index ea4aa2e3..e8c14434 100644
--- a/src/modlist.cpp
+++ b/src/modlist.cpp
@@ -849,7 +849,7 @@ void ModList::modInfoChanged(ModInfo::Ptr info)
}
int row = ModInfo::getIndex(info->name());
- info->testValid();
+ info->diskContentModified();
emit aboutToChangeData();
emit dataChanged(index(row, 0), index(row, columnCount()));
emit postDataChanged();
@@ -1076,7 +1076,7 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa
}
if (!modInfo->isValid()) {
- modInfo->testValid();
+ modInfo->diskContentModified();
}
return true;
diff --git a/src/modlist.h b/src/modlist.h
index d8980dec..4d456a9a 100644
--- a/src/modlist.h
+++ b/src/modlist.h
@@ -276,8 +276,6 @@ protected:
private:
- bool testValid(const QString &modDir);
-
QVariant getOverwriteData(int column, int role) const;
QString getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const;
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index feab76a7..6e4b3fbf 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -208,7 +208,8 @@ void OrganizerCore::updateExecutablesList()
void OrganizerCore::updateModInfoFromDisc() {
ModInfo::updateFromDisc(
m_Settings.paths().mods(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.interface().displayForeign(), managedGame());
+ m_PluginContainer, m_Settings.interface().displayForeign(),
+ m_Settings.refreshThreadCount(), managedGame());
}
void OrganizerCore::setUserInterface(IUserInterface* ui)
@@ -1097,7 +1098,8 @@ void OrganizerCore::refreshModList(bool saveChanges)
ModInfo::updateFromDisc(
m_Settings.paths().mods(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.interface().displayForeign(), managedGame());
+ m_PluginContainer, m_Settings.interface().displayForeign(),
+ m_Settings.refreshThreadCount(), managedGame());
m_CurrentProfile->refreshModStatus();
@@ -1629,7 +1631,7 @@ void OrganizerCore::syncOverwrite()
qApp->activeWindow());
if (syncDialog.exec() == QDialog::Accepted) {
syncDialog.apply(QDir::fromNativeSeparators(m_Settings.paths().mods()));
- modInfo->testValid();
+ modInfo->diskContentModified();
refreshDirectoryStructure();
}
}
diff --git a/src/thread_utils.h b/src/thread_utils.h
new file mode 100644
index 00000000..f0067b6a
--- /dev/null
+++ b/src/thread_utils.h
@@ -0,0 +1,103 @@
+#ifndef MO2_THREAD_UTILS_H
+#define MO2_THREAD_UTILS_H
+
+#include <functional>
+#include <mutex>
+#include <thread>
+
+namespace MOShared {
+
+/**
+ * Class that can be used to perform thread-safe memoization.
+ *
+ * Each instance hold a flag indicating if the current value is up-to-date
+ * or not. This flag can be reset using `invalidate()`. When the value is queried,
+ * the flag is checked, and if it is not up-to-date, the given callback is used
+ * to compute the value.
+ *
+ * The computation and update of the value is locked to avoid concurrent modifications.
+ *
+ * @tparam T Type of value ot memoized.
+ * @tparam Fn Type of the callback.
+ */
+template <class T, class Fn = std::function<T()>>
+struct MemoizedLocked {
+
+ template <class Callable>
+ MemoizedLocked(Callable &&callable, T value = {}) :
+ m_Fn{ std::forward<Callable>(callable) }, m_Value{ std::move(value) } { }
+
+ template <class... Args>
+ T& value(Args&&... args) const {
+ if (m_NeedUpdating) {
+ std::scoped_lock lock(m_Mutex);
+ if (m_NeedUpdating) {
+ m_Value = std::invoke(m_Fn, std::forward<Args>(args)... );
+ m_NeedUpdating = false;
+ }
+ }
+ return m_Value;
+ }
+
+ void invalidate() {
+ m_NeedUpdating = true;
+ }
+
+private:
+ mutable std::mutex m_Mutex;
+ mutable std::atomic<bool> m_NeedUpdating{ true };
+
+ Fn m_Fn;
+ mutable T m_Value;
+};
+
+/**
+ * @brief Apply the given callable to each element between the two given iterators
+ * in a parallel way.
+ *
+ * The callable should be independent, or properly synchronized, and the source of
+ * the range should not change during this call.
+ *
+ * @param start Beginning of the range.
+ * @param end End of the range.
+ * @param callable Callable to apply to every element of the range. See std::invoke
+ * requirements. Must be copiable.
+ * @param nThreads Number of threads to use.
+ *
+ */
+template <class It, class Callable>
+void parallelMap(It begin, It end, Callable callable, std::size_t nThreads)
+{
+ std::mutex m;
+ std::vector<std::thread> threads(nThreads);
+
+ // Create the thread:
+ // - The mutex is only used to fetch/increment the iterator.
+ // - The callable is copied in each thread to avoid conflicts.
+ for (auto &thread: threads) {
+ thread = std::thread([&m, &begin, end, callable]() {
+ while (true) {
+ decltype(begin) it;
+ {
+ std::scoped_lock lock(m);
+ if (begin == end) {
+ break;
+ }
+ it = begin++;
+ }
+ if (it != end) {
+ std::invoke(callable, *it);
+ }
+ }
+ });
+ }
+
+ // Join everything:
+ for (auto& t : threads) {
+ t.join();
+ }
+}
+
+}
+
+#endif