Synchronisation von Prozessen

Martin Kompf

Beim Austausch von Daten zwischen unabhängig ablaufenden Prozessen können seltsame Resultate aufgrund des asynchronen Zugriffs auf gemeinsam genutzte Ressourcen auftreten. Eine Synchronisation der Prozesse durch Verwendung von Sperrmechanismen wie Semaphoren schafft Abhilfe.

Hinweis: Der in diesem Artikel gezeigte Sourcecode kann als Zipfile heruntergeladen werden.

Der Artikel Memory Mapped Files zeigt, wie zwei Programme Daten über gemeinsam genutzten Speicher austauschen können. Das eine Programm mapt schreibt periodisch Daten in ein Feld, das zweite Programm maps liest diese Daten, um sie auszugeben und die Summe zu berechnen. Ein Anwendungsfall dafür könnte zum Beispiel ein Buchungssystem sein: Ein Programm erstellt Buchungen und ein zweites liest diese aus dem gemeinsam genutzten Speicher, um damit diverse Kalkulationen auszuführen.

Dabei kann ein schwerwiegendes Problem auftreten: Während maps bei der Berechnung der Summe aller Zahlen von vorn nach hinten das Feld durchläuft, könnte mapt eine Zahl in genau diesem Feld verändern! Falls die veränderte Zahl nun bereits in die Summenberechnung eingeflossen ist, dann wird die ausgegebene Summe falsch sein! Um dies zu veranschaulichen, werden einige kleine Veränderungen an den Programmen vorgenommen. So berechnet mapt auch schon die Summe und trägt sie in das Feldelement an Position 199 ein:

/* ... */

#ifdef _WIN32
#include <windows.h>

void sleep( int x) { Sleep( 1000*x); }
void usleep( int x) { Sleep( x/1000); }
#endif

/* ... */

int main( int argc, char **argv)
{
    int rc, i, sum;
    int *a;
    mappedRegion reg;

    rc = memmap( "xxxmapfilexxx", 12345, 200*sizeof(int), & reg);
    a = (int *)memGetAddr( & reg);

    srand( time(0));
    for (;;) {
        a[irand(0,198)] = irand(-1000, 1000);
        /* compute sum and put it into a[199] */
        for (i=0, sum=0; i<199; ++i) {
            usleep( 10000); /* to increase the likelihood of the problem */
            sum += a[i];
        }
        a[199] = sum;

        sleep(1);
    }
}

Das Programm maps vergleicht nun die von ihm berechnete Summe mit dem Feldelement an Position 199 (d.h. der von mapt ermittelten Summe):

/* ... */

int main( int argc, char **argv)
{
    int rc, i;
    int *a;
    int sum;
    mappedRegion reg;

    rc = memmap( "xxxmapfilexxx", 12345, 200*sizeof(int), & reg);
    a = (int *)memGetAddr( & reg);

    for (i=0; i<200; ++i) {
        if (i%10 == 0) printf("\n");
        printf( "%6d", a[i]);
    }

    /* compute sum and compare it with a[199] */
    for (i=0, sum=0; i<199; ++i) {
        sum += a[i];
    }
    printf("\n Summe = %d\n", sum);
    if (sum != a[199]) printf( " *** Fehler, %d != %d\n", sum, a[199]);

    memunmap( & reg);
}

Normalerweise sollten die von mapt und maps berechneten Summen gleich sein - jeder Buchhalter würde dies erwarten. Zum Glück rechnen wir nach und tatsächlich erscheinen sporadisch Meldungen wie

 Summe = 539
 *** Fehler, 539 != 1913

Das ist die Bestätigung der oben aufgestellten These, dass asynchroner Zugriff auf gemeinsam genutzte Ressourcen zu Fehlern führen kann.

Doch wie kann dem abgeholfen werden? Zum Beispiel dadurch, indem während des Zugriffs auf den gemeinsam genutzten Speicher dieser exklusiv gesperrt wird. Während dieser Sperre kann kein anderer Prozess auf diesen Speicher zugreifen (zumindest dann nicht, wenn er sich an die Vereinbarungen bezüglich der verwendeten Sperre hält). Eine Methode zur Realisierung von Sperren sind Semaphore. Diese sind auf allen populären Betriebssystemen vorhanden, leider (natürlich!) unterscheidet sich die Implementierung von Microsoft total von der für Unix vorhandenen. Somit ist man gut beraten, den für Sperrmechanismen notwendigen Programmcode wieder in ein eigenes Modul - zum Beispiel namens memlock - zu packen.

Zunächst sei hier die Headerdatei memlock.h aufgelistet:

#ifndef memlock_h
#define memlock_h 1

#ifdef _WIN32
#include <windows.h>
typedef HANDLE MEMLOCK;
#else
#include <unistd.h>
typedef int MEMLOCK;
#endif

/* create a new semaphore 
 * Input Parameter:
 *   key_t : system unique key of the semaphore
 * Output Parameter:
 *   s  : the handle of the semaphore
 * Return value:
 *    0 : OK 
 *   -1 : error
 */
int memLockCreate( int key, MEMLOCK *s);


/* open an existing semaphore 
 * Input Parameter:
 *   key_t : system unique key of the semaphore
 * Output Parameter:
 *   s  : the handle of the semaphore
 * Return value:
 *    0 : OK 
 *   -1 : error
 */
int memLockOpen( int key, MEMLOCK *s);


/* destroy a semaphore 
 * Input Parameter:
 *   s : the semaphore handle
 */
void memLockDestroy( MEMLOCK s);


/* request a lock 
 * Input Parameter:
 *   s : the semaphore handle
 * Return value:
 *   0 : OK
 *   else : error 
 */
int memLock( MEMLOCK s);


/* release a lock
 * Input Parameter:
 *   s : the semaphore handle
 * Return value:
 *   0 : OK
 *   else : error 
 */
int memUnlock( MEMLOCK s);


#endif

Die Implementierung des Moduls steht in memlock.c:

#include <stdio.h>

#include "memlock.h"

#ifndef _WIN32
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#endif

int memLockCreate( int key, MEMLOCK *s)
{
#ifdef _WIN32
    char semname[30];
    sprintf( semname, "sem%08x", key);
    *s = CreateSemaphore( NULL, 1, 1, semname);
    return (*s != NULL)? 0 : -1;
#else
    *s = semget( key, 1, IPC_CREAT|0600);
    if (*s > 0) memUnlock( *s);
    return (*s >= 0)? 0 : -1;
#endif
}

int memLockOpen( int key, MEMLOCK *s)
{
#ifdef _WIN32
    char semname[30];
    sprintf( semname, "sem%08x", key);
    *s = OpenSemaphore( SEMAPHORE_ALL_ACCESS, FALSE, semname);
    return (*s != NULL)? 0 : -1;
#else
    *s = semget( key, 1, 0);
    return (*s >= 0)? 0 : -1;
#endif
}
    
void memLockDestroy( MEMLOCK s)
{
#ifdef _WIN32
    CloseHandle( s);
#else
    semctl( s, 0, IPC_RMID);
#endif
}

int memLock( MEMLOCK s)
{
#ifdef _WIN32
    return (WAIT_FAILED == WaitForSingleObject( s, INFINITE));
#else
    struct sembuf sop;

    sop.sem_num = 0;
    sop.sem_op = -1;
    sop.sem_flg = 0;

    return semop( s, &sop, 1);
#endif
}

int memUnlock( MEMLOCK s)
{
#ifdef _WIN32
    return !ReleaseSemaphore ( s, 1, NULL);
#else
    struct sembuf sop;

    if (semctl( s, 0, GETVAL) > 0)
        return; /* already unlocked */

    sop.sem_num = 0;
    sop.sem_op = 1;
    sop.sem_flg = 0;

    return semop( s, &sop, 1);
#endif
}

Dieses Modul enthält Routinen für die Erzeugung (memLockCreate bzw. memLockOpen) und Zerstörung (memLockDestroy) von Semaphoren sowie für das Setzen (memLock) und Lösen (memUnlock) von Sperren. Das Sperrverhalten wird hauptsächlich durch die Funktion memLock() gesteuert: Dasjenige Programm, welches zuerst memLock() aufruft, bekommt die Sperre zugeteilt. Jedes weitere Programm, welches jetzt auch memLock() aufruft, blockiert solange, bis der Inhaber der Sperre seinerseits memUnlock() aufruft und damit die Sperre freigibt. Jetzt bekommt der nächste Aufrufer von memLock() die Sperre zugeteilt und so weiter.

Nun kann der Sperrmechanismus in unsere Programme eingebaut werden. Zunächst wird mittels

#include "memlock.h"

die entsprechende Headerdatei inkludiert. Weiterhin legen wir fest, dass das Programm mapt für das Anlegen der Semaphore verantwortlich ist. Vor dem Zugriff auf den gemeinsam genutzten Speicher wird dann mittels dieser Semaphore eine Sperre gesetzt, die nach Beendigung der Summenberechnung wieder freigegeben wird. Der entsprechende Programmteil von mapt sieht dann folgendermaßen aus:

/* ... */
int main( int argc, char **argv)
{
    MEMLOCK s;
    /* ... */

    if ( memLockCreate( 12345, & s) < 0) {
        printf( "memLockCreate failed\n");
        exit(1);
    }

    srand( time(0));
    for (;;) {
        memLock(s);
        a[irand(0,198)] = irand(-1000, 1000);
        /* compute sum and put it into a[199] */
        for (i=0, sum=0; i<199; ++i) {
            usleep( 10000); /* to increase the likelihood of the problem */
            sum += a[i];
        }
        a[199] = sum;
        memUnlock(s);

        sleep(1);
    }
    memLockDestroy(s);
}

Analog wird in maps verfahren, nur dass hier selbstverständlich keine neue Semaphore erzeugt werden darf, sondern die von mapt verwendete genommen werden muss:


/* ... */
int main( int argc, char **argv)
{
    MEMLOCK s;
    /* ... */

    if (memLockOpen( 12345, & s) < 0) {
        printf( "memLockOpen failed\n");
        exit(1);
    }

    memLock(s);
    for (i=0; i<200; ++i) {
        if (i%10 == 0) printf("\n");
        printf( "%6d", a[i]);
    }

    /* compute sum and compare it with a[199] */
    for (i=0, sum=0; i<199; ++i) {
        sum += a[i];
    }
    printf("\n Summe = %d\n", sum);
    if (sum != a[199]) printf( " *** Fehler, %d != %d\n", sum, a[199]);
    memUnlock(s);

    memunmap( & reg);
}

Wird jetzt mapt auf einer eigenen Konsole gestartet und mittels maps der Inhalt des gemeinsamen Speichers ausgegeben, dann kommt es zu keinen Fehlern bei der Summenberechnung mehr. Jedoch tritt nun manchmal der Effekt auf, dass maps nach dem Start eine »Denkpause« einlegt, bevor die Ausgabe erscheint. Das ist dann genau die Situation, in der mapt noch die Sperre gesetzt hat und maps in der Funktion memLock() hängt und darauf wartet, das mapt seinerseits memUnlock() aufruft.

Der Gewinn an Sicherheit (es wird immer die korrekte Summe ausgegeben) geht also mit einem Verlust der Performance einher. Diesen Performanceverlust könnte man durch geeignete Optimierungen des Programmablaufs sicher noch verringern, jedoch zeigt dieses Beispiel eindrucksvoll einen zentralen Aspekt der Entwicklung verteilter Software: Performance und Sicherheit hängen zusammen und wirken entgegengesetzt - Verbesserungen an der einen Komponente bewirken eine Verschlechterung der anderen. Wo das ausgewogene Mittel zwischen Performance und Sicherheit liegt, kann allein anhand der konkreten Anwendung entschieden werden: Eine Buchhaltungsanwendung wird hier andere Anforderungen haben als ein Spielprogramm.