Aufteilung einer Aufgabe an mehrere Threads

Wenn parallelisiert wird, dann eröffnet sich die Frage, wieviel Threads wir sinnvollerweise erzeugen und wie eine zu erledigende Aufgabe entsprechend aufgeteilt wird. Zu diesem Zeitpunkt gehen wir davon aus, dass die Aufgaben sich zu Beginn aufteilen lassen, so dass die einzelnen Teilaufgaben unabhängig voneinander bearbeiten lassen. Dann genügt uns das vorgestellte Fork-and-Join-Pattern.

Grundsätzlich können im Rahmen der zur Verfügung stehenden Ressourcen beliebig viele Threads erzeugt werden -- auch deutlich mehr als von der Hardware unterstützt werden. Dann müssen wir davon ausgehen, dass die rechenintensiven Threads um die zur Verfügung stehenden Ressourcen konkurrieren. Da Kontextwechsel nicht ganz billig sind, leidet darunter die Performance. Die Zahl der rechenintensiven Threads sollte daher begrenzt werden, auf das, was die Hardware unterstützt bzw. auf das, was ein Benutzer per Parameter festlegt.

Der C++11-Standard bietet eine Funktion an, um die Hardware-Unterstützung abzufragen:

#include <thread>
#include <cstdio>

int main() {
   printf("hardware concurrency = %u\n", std::thread::hardware_concurrency());
}
$shell> g++ -std=c++11 -o concurrency concurrency.cpp
$shell> ./concurrency
hardware concurrency = 24

Aufgabe

Initialisieren Sie wie zuvor eine Matrix mit Zufallszahlen. Diesmal sollte es aber nicht auf zwei Threads aufgeteilt werden, sondern auf die Zahl von Threads, die von std::thread::hardware_concurrency() zurückgeliefert wird. Die Aufteilung sollte dabei möglichst gleichmäßig erfolgen.

Wenn Sie eine variable Zahl von Threads erzeugen, ist hierfür die Datenstruktur eines Thread-Arrays notwendig. Da die Zahl der Threads erst zur Laufzeit erzeugt wird, empfiehlt sich hier die Verwendung der Klasse std::vector aus der C++-Standardbibliothek, die über <vector> zugänglich ist:

unsigned int nof_threads = std::thread::hardware_concurrency();
std::vector<std::thread> threads(nof_threads);
for (int index = 0; index < nof_threads; ++index) {
   threads[index] = std::thread(/* lambda expression */);
}

In der std::vector<std::thread>-Deklaration wird ein Array mit nof_threads Elementen angelegt. All die Threads-Objekte sind zu dem Zeitpunkt noch leere Hüllen. Erst innerhalb der for-Schleife wird den einzelnen Thread-Objekten ein neu erzeugtes Thread-Objekt zugewiesen, so dass sie ab da für die entsprechenden Threads Verantwortung tragen.

Überlegen Sie sich bei der Aufgabe genau, wo Sie die einzelnen Sichten auf die Teilmatrizen erzeugen und wie Sie diese in der capture des Lambda-Ausdrucks übergeben.

Testen Sie Ihre Lösung mit kleinen und ungeraden Matrix-Größen.