Ein Muss für Skalierung

Going Parallel with C++
Kommentare

Multi-Core, Vector-Unit und GPU – moderne Hardware bietet heute mehr, als die meiste Software nutzt. Daher ist vielerorts Nachrüsten angesagt, Parallelisierung wird Pflicht. Doch keine Angst, moderne Entwicklungswerkzeuge und Bibliotheken machen dies auch für C++-Programmierer zu einer durchaus zu bewältigenden Aufgabe.

Windows Developer

Der Artikel „Ein Muss für Skalierung“ von Thomas Trotzki und Christian Binder ist erstmalig erschienen im Windows Developer 10.2012

Die Zeiten, in denen eine einzelne Anwendung das komplette Betriebssystem lahmlegen konnte, sind lange vorbei – zu guter Letzt auch dank eines entsprechenden Parallelisierungskonzepts auf Systemebene. In Windows ist das seit Einführung von Windows NT der Fall. Mit NT 3.1 kamen im Jahr 1991 Processes und Threads sowie die Unterstützung des symmetrischen Multiprocessings. In den Folgeversionen kamen weitere Konzepte wie Fibers, Jobs und Thread Pools hinzu – alles Dinge, die es uns Entwicklern einfacher machen sollen, die Multiprocessing-Fähigkeit des Systems zu nutzen. Das neueste Kind dieser Reihe: User Mode Scheduled Threads aus Windows 7.

Im selben Zeitraum hat sich auch auf Ebene der Hardware einiges getan. Vor 20 Jahren waren bei PCs Systeme mit einer CPU und 33 MHz Taktrate angesagt. Damals wurde die Performanceverbesserung der Systeme nahezu ausschließlich mit einer Erhöhung der Taktrate gleichgesetzt. Physikalische Mehrprozessorsysteme gab es zwar, sie waren aber doch eher die Exoten. Somit konnte Parallelisierung zwar so manches andere Problem lösen, Performancesteigerung gehörte aber nicht unbedingt dazu. Seit ca. 5 Jahren hat sich das grundlegend geändert, Hyperthreading und Multicore gibt es heute beim Discounter. Im Gegenzug dazu hat sich die Taktrate der Systeme nur noch eingeschränkt erhöht. Nicht mehr MHz und GHz zählen, sondern die Anzahl der Cores. Und wer davon profitieren möchte, der kommt an Parallelisierung eben nicht mehr vorbei.

Artikelserie

  1. Modern C++11
  2. Going Parallel with C++
  3. ALM for C++

Das Problem: So richtig schick ist Multiprocessing mit dem klassischen Windows-API nicht unbedingt zu programmieren, sind doch Themen wie die Übergabe von Parametern und Synchronisierung recht schwierig umzusetzen. Und wenn wir in Richtung massive Parallelisierung auf einer GPU weiterdenken, dann sind wir in einer anderen Welt gelandet, die mit C++ eigentlich nichts mehr zu tun hat. Hier tut eine Neuerung dringend Not.

Die Parallel Pattern Library

Für die .NET-Programmierer gibt es die Task Parallel Library (TPL), diese wurde in den letzten Ausgaben ausführlich vorgestellt. Für C++-Programmierer bietet Microsoft die Concurrency Runtime (ConcRT), die die Konzepte der TPL analog in die unmanged Welt bringt. Sinn und Zweck dieser Bibliotheken ist die Abstraktion der Parallelisierung und damit eine Unabhängigkeit von den zu unterstützenden Windows-Zielplattformen. Werden auf der Zielplattform UMS Threads unterschützt, werden sie verwendet, andernfalls greift die Library auf herkömmlich Threads zurück. Die ConcRT besteht aus mehreren Teil-Komponenten:

  • Resource Manager: verwaltet Ressourcen wie Speicher und Prozessoren.
  • Task Scheduler: stellt den Scheduler zur Verfügung, der die anstehenden Anforderungen auf die zur Verfügung stehenden Ressourcen verteilt.
  • Parallel Pattern Library (PPL): bietet die beiden wesentlichen Aspekte „parallele Schleifen“ und „Tasks“. Diese abstrahieren die Parallelisierungsmechanismen des Systems.
  • Asynchronous Agents Library: bietet Unterstützung bei der asynchronen Bearbeitung von Datenblöcken.

Im Wesentlichen ist es die PPL, die mit der TPL zu vergleichen ist. Resource Manager und Task Scheduler werden sie eher selten direkt verwenden, und auch die Agents Library wird deutlich weniger Anwendungsfälle finden.

Lambdas

Lambdas sind ein neues Sprachmerkmal in C++ 11 und wurden in der letzten Ausgabe ausführlich vorgestellt [1]. Sie spielen auch bei der Nutzung der PPL eine wesentliche Rolle, werden doch alle „Thread-Prozeduren“ in der PPL durch Lambdas ausgedrückt.

Tasks, der einfache Weg zur Parallelisierung

Im Gegensatz zum klassischen Win32 wird bei der Nutzung der PPL nicht unmittelbar von Threads gesprochen, sie sind lediglich eine – wenngleich recht wichtige – Umsetzungsoption für die Parallelisierung. Ein Task stellt eine elementare Ausführeinheit innerhalb der PPL dar und wird vom Task Scheduler der ConcRT verteilt. Bei dieser Verteilung berücksichtigt der Scheduler die auf dem Zielsystem zur Verfügung stehenden Ressourcen. Zur Veranschaulichung ein Beispiel: Sind zu einem konkreten Zeitpunkt weit mehr Tasks „ausführbereit“, als es physikalische Recheneinheiten gibt, so wird der Task Scheduler nicht für jeden Task einen separaten System-Thread erstellen, sondern je nach Zielsystem die Tasks entweder sequenziell abarbeiten oder mehrere Tasks über UMS Threads zur Ausführung bringen. In den meisten Anwendungsfällen ist es tatsächlich so, dass wir Entwickler uns nicht um diesen Sachverhalt kümmern müssen und wollen. Die Einsatzszenarien für Paralleliserung sind vielfältig, Listing 1 zeigt eine kleine Codesequenz.

Listing 1

int _tmain(int argc, _TCHAR* argv[])
{
  vector v = get_sudoku_samples();
  const int nFields = v.size();

  for(int field = 0; field < nFields; field++)
  {
    int (* sudoku) [9] = v[field];
    int values = sudoku_getcount(sudoku);
    int solutions = sudoku_solve(sudoku, 0, 0);
    cout << field << " (" << values << "," 
         << solutions << ")" << endl;
    delete [] sudoku;
  }

  return 0;
}  

Das Kernstück ist der Aufruf der Methode sudoku_solve, die die Anzahl der möglichen Lösungen eines Sudoku-Rätsels liefert. (Hinweis: Für alle, die wissen wollen, wie Sudoku-Rätsel per Computer gelöst werden, steht der kleine Backtracking-Algorithmus als Download auf www.windowsdeveloper.de bereit) Diese Berechnung kann dauern, vor allem, wenn Sie, wie dargestellt, gleich mehrere Rätsel zu lösen haben. Mit der PPL wird die Parallelisierung recht einfach, betrachten Sie hierzu das nächste Code-Snippet in Listing 2.

Listing 2

int _tmain(int argc, _TCHAR* argv[])
{
  vector v = get_sudoku_samples();
  const int nFields = v.size();

  task *tasks = new task[nFields];
  critical_section csCout;
  for(int field = 0; field < nFields; field++)
  {
    tasks[field] = task([=, &csCout] () -> void 
    {
      int (* sudoku) [9] = v[field];
      int values = sudoku_getcount(sudoku);
      int solutions = sudoku_solve(sudoku, 0, 0);
      csCout.lock();
      cout << field << " (" << values << "," 
           << solutions << ")" << endl;
      csCout.unlock();
      delete [] sudoku;
    });
  }

  when_all(tasks, tasks+nFields).wait();
  delete [] tasks;

  return 0;
}  
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -