Multithreading - JThreads/C++

Martin Kompf

Die Entwicklung von multithreaded C++-Programmen unter Windows und Unix lässt sich unter Zuhilfenahme des Paketes JThreads/C++ relativ einfach bewerkstelligen. Dies wird anhand einer kompletten Beispielapplikation gezeigt.

Wichtiger Hinweis: Dieser Artikel ist veraltet! Das geniale Paket JThreads/C++, ursprünglich als Teil von ORBacus von Object Oriented Concepts entwickelt, ist leider nicht mehr allgemein verfügbar. Als Alternative bietet sich QThread an, welches Bestandteil des Qt-Paketes von Trolltech ist.

Nach der Einführung in das Thema Multithreading soll nun die Programmierung einer mit mehreren Threads arbeitenden Applikation demonstriert werden. Dazu werden wir das bereits erwähnte Paket JThreads/C++ zu Hilfe nehmen, welches das Erstellen portabler multithreaded Programme unter Windows und den meisten Unixderivaten drastisch vereinfacht. Das Paket liegt im Sourcecode vor und ist für die nicht kommerzielle Verwendung kostenlos.

Die Beispielapplikation

Unsere Beispielapplikation soll im Prinzip genau dasselbe tun, wie die im Artikel Synchronisation von Prozessen vorgestellte Anwendung - nur mittels paralleler Threads anstelle mehrerer Prozesse: Ein Thread soll in ein Feld (das »Checkboard«) zufällig Zahlen eintragen und die Summe aller im Checkboard stehenden Zahlen ermitteln und ebenfalls eintragen. Ein zweiter Thread gibt das Checkboard regelmäßig auf Standardausgabe aus und überprüft die berechnete Summe. Das vorgestellte Programm lässt sich ohne Änderungen sowohl unter Windows mit dem Visual C++ Compiler 6.0 und unter Linux mit dem GNU-Compiler 2.95 übersetzen und ausführen. Die Codeausschnitte werden im folgenden in ihrem logischen Kontext vorgestellt und kommentiert, die Aufteilung von Klassendefinitionen und -implementierungen auf die entsprechenden *.h und *.cpp Dateien sei dem Leser überlassen. Der vollständige Sourcecode steht ausserdem als Zip-Archiv hier zur Verfügung.

Zunächst definieren wir die Klasse Checkboard, die (nomen est omen) das Checkboard beherbergt und die Funktionen entry() für den zufälligen Eintrag einer Zahl und printAndCheck() für die Ausgabe und Verifizierung des Boards zur Verfügung stellt:

class Checkboard {
public:
    Checkboard( int size);
    void entry();
    void printAndCheck();
 
private:
    int* board_;
    int size_;
};

Die Implementierung der Klasse gestaltet sich relativ einfach, um später das Synchronisationsproblem zu forcieren, fügen wir in die entry() Funktion noch ein zusätzliches sleep von 1 ms ein. Die Funktion irand() ist dem Artikel Zufallszahlen entnommen:

Checkboard::Checkboard( int size)
 : board_ (new int[size]), size_ (size)
{
    for (int i = 0; i < size_; ++i) board_[i] = 0;
}
 
void Checkboard::entry()
{
    int sum, i;
 
    board_[irand(0, size_-2)] = irand(-1000, 1000);
    // compute sum and put it into last element of board_ 
    for (i = 0, sum = 0; i < size_-1; ++i) {
        JTCThread::currentThread() -> sleep( 1); // to force the problem
        sum += board_[i];
    }
    board_[size_-1] = sum;
}
 
void Checkboard::printAndCheck()
{
    int sum, i;
 
    for (i = 0; i < size_; ++i) {
        if (i % 10 == 0) cout << endl;
        cout << setw(6) << board_[i];
    }
 
    // compute sum and compare it with last element of board_
    for (i = 0, sum = 0; i < size_-1; ++i)
        sum += board_[i];
 
    cout << "\n Summe = " << sum << endl;
    if (sum != board_[size_-1])
        cerr << " *** Fehler, " << sum << " != " << board_[size_-1] << endl;
 
}

Threads lassen sich ganz leicht erzeugen

Als nächstes müssen wir uns Gedanken um die Threaderzeugung machen. JThreads/C++ übernimmt dabei die meiste Arbeit für uns und stellt eine Klasse JTCThread zur Verfügung. Um ein eigenes vom Benutzer erzeugtes Objekt als Thread laufen zu lassen, genügt es, eine Klasse für dieses Objekt zu definieren, die von JTCThread abgeleitet ist und eine Methode run() implementiert. Für den Thread, der die Einträge ins Checkboard vornimmt, sieht die Klassendefinition also folgendermaßen aus:

#include <JTC/JTC.h>
 
class CheckboardEntryThread : public JTCThread {
 
public:
    CheckboardEntryThread( Checkboard* cb_);
    virtual void run();
 
private:
    Checkboard* cb_;
};

Im Konstruktor dieser Klasse wird ein Pointer auf das Checkboard übergeben. Die Implementierung der run() Methode beschränkt sich im wesentlichen auf eine Endlosschleife, in der die entry() Funktion des Checkboardobjekts periodisch aufgerufen wird:

CheckboardEntryThread::CheckboardEntryThread( Checkboard* cb)
 : cb_ (cb)
{ }
 
void CheckboardEntryThread::run()
{
    srand( time(0));
    for (;;) {
        cb_ -> entry();
        this -> sleep(100);
    }
}

Analog sehen Definition und Implementierung des zweiten Threads aus, der für die regelmäßige Ausgabe des Checkboards und die Überprüfung der Summe verantwortlich ist:

class CheckboardPrinterThread : public JTCThread {
 
public:
    CheckboardPrinterThread( Checkboard* cb_);
    virtual void run();

private:
    Checkboard* cb_;
};
 
CheckboardPrinterThread::CheckboardPrinterThread( Checkboard* cb)
 : cb_ (cb)
{ }
 
void CheckboardPrinterThread::run()
{
 
    for (;;) {
        cb_ -> printAndCheck();
        this -> sleep(5000);
    }
}

Nun haben wir bereits alle notwendigen Klassen definiert und implementiert, so dass jetzt das Hauptprogramm geschrieben werden kann. Als erstes erfolgt die Initialisierung des JThreads/C++ Paketes und die Konstruktion eines Checkboards mit 200 Einträgen:

#include <iostream.h>
#include <JTC/JTC.h>
 
int main ( int argc, char **argv)
{
    JTCInitialize initialize;
 
    const int size = 200;
    Checkboard* theBoard = new Checkboard( size);

Objektorientiertes Design hilft

Nun kann es an das Erzeugen der beiden Threads gehen. Dazu werden die beiden Threadobjekte entry und printer durch Instanzierung der von uns definierten, von JTCThread abgeleiteten Klassen, CheckboardEntryThread und CheckboardPrinterThread ins Leben gerufen. Durch Aufruf der Methode start() wird dann der entsprechende Thread gestartet. Diese Methode wurde nicht von uns implementiert, sie entstammt der Basisklasse JTCThread und ruft ihrerseites unser run() auf:

    JTCThread* entry = new CheckboardEntryThread( theBoard);
    JTCThread* printer = new CheckboardPrinterThread( theBoard);
    printer -> start();
    cout << "printer started\n";
    entry -> start();
    cout << "entry started\n";
 
    return 0;
}

Die Verwendung des Paketes JThreads/C++ ist ein Musterbeispiel, wie durch den Einsatz objektorientierter Technologien die Entwicklung kompletter Applikationen radikal vereinfacht wird: Durch Ableitung von einer Basisklasse und Implemntierung einer virtuellen Funktion wird ohne weiteres Zutun eine mächtige Funktionalität wie Multithreading implementiert - und das unter Windows und Unix gleichermaßen!

Unbedingt: Synchronisation

Lässt man das Programm nun ablaufen, stellt sich aber bald Ernüchterung ein. Regelmäßig wird eine Fehlerausgabe produziert, die darauf hinweist, dass die Summe der Zahlen im Checkboard nicht stimmt. Wer die Artikel Synchronisation von Prozessen und Multithreading - Einführung aufmerksam gelesen hat, kennt jedoch schon die Ursache für dieses Problem: Die beiden Threads unseres Programmes laufen asynchron nebeneinander her, benutzen jedoch das gleiche gemeinsame Checkboardobjekt! Dabei kann es vorkommen, dass der eine Thread noch mit dem Modifizieren des Boards befasst ist, während der andere Thread dieses bereits ausgibt und die Summe verifiziert. Die Lösung hierfür ist, dass die beiden Threads synchronisiert werden müssen. Dafür stellt JThreads/C++ die Klasse JTCMonitor zur Verfügung. Da die Zugriffe auf das Checkboard synchronisiert werden müssen, leitet man am besten Checkboard direkt von JTCMonitor ab:

class Checkboard : public JTCMonitor {
    // see above
    // ...

Nun muss nur noch am Anfang der kritischen Funktionen, die auf das Checkboard zugreifen, ein Objekt vom Typ JTCSynchronized deklariert werden:

void Checkboard::entry()
{
    JTCSynchronized synchronized(*this);
    // see above
    // ...
 
void Checkboard::printAndCheck()
{
    JTCSynchronized synchronized(*this);
    // see above
    // ...

Dies bewirkt, dass sobald ein Thread in die Funktion entry() eintritt, alle weiteren Threads davon abgehalten werden, in die Funktionen entry() oder printAndCheck() zu gehen. Sie blockieren solange, bis der erste Thread entry() verlassen hat. Damit wird eine zuverlässige, allerdings relativ »grobkörnige« (es wird immer das ganze Objekt gesperrt) Synchronisation erreicht.