next up previous contents
Nächste Seite: Implementation des Windischer Threads Aufwärts: User Mode Threads Vorherige Seite: Warum Parallelprogrammierung?   Inhalt

Stammfunktionen (primitives) für Threads

Damit wir über die Programmierung sprechen können, müssen wir zuerst einige Stammfunktionen festlegen. Im wesentlichen geht es um:



Erzeugen von Threads

Ein Thread wird erzeugt, indem man wta_create aufruft. Durch diesen Aufruf wird ein Thread erzeugt, der asynchron mit dem aktuellen Thread die spezifizierte Prozedur auszuführen beginnt. wta_create liefert einen Handle auf den erzeugten Thread. Dieser Handle kann der Prozedur wta_join als Argument übergeben werden. Mit wta_join wird auf das Beenden des betreffenden Threads gewartet.

#include "wthread.h"

int
wta_create (wta_t_thread        *new_thread,
                void            (*start_routine)(),
                unsigned        wspsize,
                unsigned        priority,
                unsigned        slice_count);

/* Als Funktionswert von 'wta_create' erhaelt man:
        SUCCESS         => Thread erfolgreich erzeugt
        NOMEMORY        => zuwenig Speicher, kein Thread erzeugt. */


int
wta_join (wta_t_thread       awaited_thread);

/* Als Funktionswert von 'wta_join' erhaelt man:
        SUCCESS         => Thread erfolgreich beendet
        NOTHREAD        => Angegebener Thread existiert nicht. */
Im nachfolgenden Programm-Fragment werden die Funktionen a(x) und b(y) parallel abgearbeitet:
#include "wthread.h"

int b(x)
int x;
{
        /* Code der Funktion b */
}

void a()
{
        /* Code der Funktion a */
}
main()
{
        wta_t_thread example_thread;

        wta_init();        /* Initialisierung des Threads Package */
        wta_create (&example_thread, a, 1024, 10, 1);
        p = b(y);
        wta_join (example_thread);
}

In der Praxis wird wta_join nicht sehr häufig angewandt. Die meisten Threads bleiben während der gesamten Lebensdauer des Prozesses erhalten. Falls ein Thread beendet, ohne dass ein wta_join aufgesetzt wurde, so verschwindet dieser Thread ohne Auswirkungen auf andere Threads. Wechselseitiger Ausschluss

Die einfachste Interaktion zwischen Threads erfolgt durch die Verwendung gemeinsamen Speichers (shared memory). Das bedeutet in höheren Sprachen den Zugriff auf globale Variablen. Um die richtige Funktionsweise der verschiedenen parallelen Threads beim Zugriff auf globale Variabeln sicherzustellen, verwendet man Variabeln vom Datentyp wta_t_mutex, auf denen die Operationen wta_lock, wta_try_lock und wta_unlock möglich sind. Erzeugt resp. Gelöscht werden diese Variabeln mit den Funktionen
wta_create_mutex und wta_delete_mutex

int
wta_create_mutex (wta_t_mutex *mutex);

/* Als Funktionswert von 'wta_create_mutex' erhaelt man:
        SUCCESS                => Mutex erfolgreich erzeugt
        NOMEMORY               => zuwenig Speicher, kein Mutex erzeugt. */


int
wta_delete_mutex (wta_t_mutex *mutex);

/* Als Funktionswert von 'wta_delete_mutex' erhaelt man:
        SUCCESS                => Mutex geloescht
        NOMUTEX                => Mutex Zeiger ungueltig
        NOACCESS               => Mutex ungueltig */


int
wta_lock (wta_t_mutex *mutex);

/* Als Funktionswert von 'wta_lock' erhaelt man:
        SUCCESS                => Mutex reserviert
        NOMUTEX                => Mutex Zeiger ungueltig
        NOACCESS               => Mutex ungueltig */

int
wta_try_lock (wta_t_mutex *mutex);

/* Als Funktionswert von 'wta_try_lock' erhaelt man:
        MUT_NOW_LOCKED          => Mutex wurde reserviert
        MUT_ALREADY_LOCKED      => Mutex bereits reserviert
        NOMUTEX                 => Mutex Zeiger ungueltig
        NOACCESS                => Mutex ungueltig */

int
wta_unlock (wta_t_mutex *mutex);

/* Als Funktionswert von 'wta_unlock' erhaelt man:
        SUCCESS                => Mutex freigegeben
        NOMUTEX                => Mutex Zeiger ungueltig
        NOACCESS               => Mutex ungueltig */

Ein 'mutex' hat zwei Zustände: gesperrt und frei. Die Stammfunktion wta_lock setzt das 'mutex' in den Zustand gesperrt; die Funktion wta_unlock setzt das 'mutex' in den Zustand frei. Von einem Thread, der die Funktion wta_lock erfolgreich ausgeführt hat, sagt man er halte das 'mutex'. Falls ein anderer Thread versucht die Operation wta_lock auszuführen auf einem bereits gesperrten 'mutex', so wird dieser Thread blockiert bis das 'mutex' frei ist.
Programmfragment zur Anwendung von wta_lock und wta_unlock:

#include "wthread.h"

typedef struct T_QUEUE {
    struct T_QUEUE        *flink;
    struct T_QUEUE     *blink;
    } t_queue;

typedef struct T_LIST {
    t_queue   list_head;
    int       list_item;
    } t_list;

extern insert_in_queue(*t_queue, *t_list);

t_queue  list_header;
t_list   list_element;
wta_t_mutex  mutex;

main()
{
        wta_init();
        wta_create_mutex(&mutex);

        /* Andere Threads koennen erzeugt werden und obige Liste verwenden */

        wta_lock (&mutex);
        insert_in_queue (&list_header, &list_element);
        wta_unlock (&mutex);
}

Im einfachsten Falle ist die 'mutex' Variable wie oben eine gewöhnliche, globale Variable. Selbstverständlich kann diese auch Teil einer Datenstruktur sein. Im obigen Beispiel wäre es sinnvoll, diese im Listenkopf einzubauen.

condition variables

Man kann ein 'mutex' als einen Mechanismus betrachten, der den Ablauf einer Betriebsmittelbenützung steuert. Das Betriebsmittel ist in diesem Falle die 'mutex'-Variable. Die Politik der Ablaufsteuerung lautet in diesem Falle: nur ein Thread aufs Mal. Oft aber ist es nötig, komplexere Ablaufsteuerungen zu erreichen. 'condition variables' beruhen auf den von T. Hoare vorgeschlagenen Monitoren. Sie dienen dazu einen Thread zu blockieren bis ein bestimmtes Ereignis eintrifft.

int
wta_create_condition (wta_t_condit *condition);

/* Als Funktionswert von 'wta_create_condition' erhaelt man:
        SUCCESS            => Condition erfolgreich erzeugt
        NOMEMORY           => zuwenig Speicher, keine Condition Variable erzeugt.*/


int
wta_delete_condition (wta_t_condit *condition);

/* Als Funktionswert von 'wta_delete_condition' erhaelt man:
        SUCCESS            => Condition Variable geloescht
        NOCONDITION        => Condition Variable Zeiger ungueltig
        NOACCESS           => Condition Variable ungueltig */

int 
wta_wait_condition (
                wta_t_condit        *condition,
                wta_t_mutex         *mutex);

/* Als Funktionswert von 'wta_wait_condition' erhaelt man:
        SUCCESS            => Condition Variable reserviert
        NOCONDITION        => Condition Variable Zeiger ungueltig
        NOACCESS           => Condition Variable ungueltig */

int
wta_signal_condition ( wta_t_condit *condition);

/* Als Funktionswert von 'wta_signal_condition' erhaelt man:
        SUCCESS            => wartender Thread befreit
        NOTHREAD           => kein Thread wartet auf diese Condition
        NOCONDITION        => Condition Variable Zeiger ungueltig
        NOACCESS           => Condition Variable ungueltig */

int
wta_signal_all_condition (wta_t_condition *condition);

/* Als Funktionswert von 'wta_signal_all_condition' erhaelt man:
        SUCCESS            => wartende(r) Thread(s) befreit
        NOTHREAD           => kein Thread wartet auf diese Condition
        NOCONDITION        => Condition Variable Zeiger ungueltig
        NOACCESS           => Condition Variable ungueltig */

Eine 'condition variable' ist immer mit einem 'mutex', und mit Daten die durch das 'mutex' geschützt werden, verbunden. Die wta_wait_condition Operation führt implizit eine wta_unlock -Operation auf dem 'mutex' aus und blockiert den Thread. Die wta_signal_condition Operation unternimmt nichts, ausser es gibt einen auf dieser 'condition variable' blockierten Thread, in diesem Falle wird ein Thread aufgeweckt (möglicherweise warten mehrere Threads). Die wta_signal_all_condition Operation arbeitet wie die wta_signal_condition Operation ausser, dass alle wartenden Threads aufgeweckt werden. Sobald ein Thread der mit wta_wait_condition gewartet hat aufgeweckt wird, so führt er erneut eine wta_lock Operation auf dem 'mutex' aus. Es ist denkbar, dass das 'mutex' zu diesem Zeitpunkt nicht frei ist; in diesem Falle blockiert der Thread bis das 'mutex' verfügbar wird. Das 'mutex', das zu einer 'condition variable' gehört, schützt die zugehörigen Daten. Falls ein Thread das Betriebsmittel will, so führt er eine wta_lock Instruktion auf dem 'mutex' aus und untersucht die gemeinsamen Daten. Falls das Betriebsmittel verfügbar ist, fährt er fort, andernfalls blockiert er sich, indem der wta_wait_condition aufruft. Später, wenn ein anderer Thread das Betriebsmittel wieder freigibt, weckt er einen allfällig wartenden mit der wta_signal_condition- oder wta_signal_all_condition-Funktion.

Das nachfolgende Programm zeigt einen Produzenten-Thread, der Elemente erzeugt und in eine Liste einfügt. Der Konsumenten-Thread liest und verarbeitet diese Elemente.

#include "wthread.h"

typedef struct T_LIST {
    struct T_LIST  *next;
    } t_list;

wta_t_mutex       mutex;
wta_t_condition   non_empty;
t_list        head, topList, newElement;

void
consumer ();
{
        for (;;) {
                /* Entnehmen eines Items aus der Liste */
                wta_lock (&mutex);
                /*
                Achtung die nachfolgende Anweisung erlaubt 'spurious wakeups'
                */
                while (head == NULL) wta_wait_condition (&mutex, &non_empty);
                topElement = head;
                head  = head->next;
                wta_unlock (&mutex);

                /* Verarbeite den Item */
                }
}
void
producer ();
{
        for (;;) {
                /* Produziere neuen Item */

                /* Fuege Item in Liste ein */

                wta_lock (&mutex);
                newElement->next = head;
                head = newElement;
                wta_signal_condition (&non_empty);
                wta_unlock (&mutex);
                }
}        

main()
{
        wta_t_thread cons_thread, prod_thread;

        wta_init();
        wta_create_mutex (&mutex);
        wta_create_condition (&non_empty);

        wta_create(&cons_thread, consumer, 1024, 10, 1);
        wta_create(&prod_thread, producer, 1024, 10, 1);

        wta_join (prod_thread); /* Warte auf die Beendigung des Produzenten */
}


next up previous contents
Nächste Seite: Implementation des Windischer Threads Aufwärts: User Mode Threads Vorherige Seite: Warum Parallelprogrammierung?   Inhalt
Hans-Peter Oser 2007-10-30