GNU/Linux >> LINUX-Kenntnisse >  >> Linux

So verwenden Sie Beispiele für C-Mutex-Sperren für die Linux-Thread-Synchronisierung

In der Reihe Linux-Threads haben wir besprochen, wie ein Thread beendet werden kann und wie der Rückgabestatus vom beendenden Thread an seinen übergeordneten Thread weitergegeben wird. In diesem Artikel werden wir etwas Licht auf einen wichtigen Aspekt werfen, der als Thread-Synchronisation bekannt ist.

Linux Threads Series:Teil 1, Teil 2, Teil 3, Teil 4 (dieser Artikel).

Thread-Synchronisierungsprobleme

Nehmen wir einen Beispielcode, um Synchronisationsprobleme zu untersuchen:

#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

pthread_t tid[2];
int counter;

void* doSomeThing(void *arg)
{
    unsigned long i = 0;
    counter += 1;
    printf("\n Job %d started\n", counter);

    for(i=0; i<(0xFFFFFFFF);i++);
    printf("\n Job %d finished\n", counter);

    return NULL;
}

int main(void)
{
    int i = 0;
    int err;

    while(i < 2)
    {
        err = pthread_create(&(tid[i]), NULL, &doSomeThing, NULL);
        if (err != 0)
            printf("\ncan't create thread :[%s]", strerror(err));
        i++;
    }

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);

    return 0;
}

Der obige Code ist ein einfacher Code, in dem zwei Threads (Jobs) erstellt werden und in der Startfunktion dieser Threads ein Zähler verwaltet wird, durch den der Benutzer die Protokolle über die Jobnummer erhält, die gestartet und wann sie abgeschlossen ist. Der Code und der Ablauf sehen gut aus, aber wenn wir die Ausgabe sehen:

$ ./tgsthreads
Job 1 started
Job 2 started
Job 2 finished
Job 2 finished

Wenn Sie sich auf die letzten beiden Protokolle konzentrieren, sehen Sie, dass das Protokoll „Auftrag 2 beendet“ zweimal wiederholt wird, während kein Protokoll für „Auftrag 1 beendet“ zu sehen ist.

Wenn Sie nun zum Code zurückkehren und versuchen, einen logischen Fehler zu finden, werden Sie wahrscheinlich keinen Fehler leicht finden. Aber wenn Sie sich die Ausführung des Codes genauer ansehen und visualisieren, werden Sie Folgendes feststellen:

  • Das Protokoll „Job 2 gestartet“ wird unmittelbar nach „Job 1 gestartet“ gedruckt, sodass leicht geschlussfolgert werden kann, dass der Planer Thread 2 geplant hat, während Thread 1 verarbeitet wurde.
  • Wenn die obige Annahme zutraf, wurde der Wert der Variable „Zähler“ erneut erhöht, bevor Job 1 beendet wurde.
  • Als also Job 1 tatsächlich fertig war, erzeugte der falsche Wert des Zählers das Protokoll „Job 2 fertig“ gefolgt von „Job 2 fertig“ für den eigentlichen Job 2 oder umgekehrt, da dies vom Planer abhängt.
  • Wir sehen also, dass nicht das sich wiederholende Protokoll, sondern der falsche Wert der „Zähler“-Variable das Problem ist.

Das eigentliche Problem war die Verwendung der Variable „counter“ durch den zweiten Thread, wenn der erste Thread sie verwendete oder im Begriff war, sie zu verwenden. Mit anderen Worten, wir können sagen, dass die fehlende Synchronisierung zwischen den Threads bei Verwendung der gemeinsam genutzten Ressource „Zähler“ die Probleme verursacht hat, oder mit einem Wort können wir sagen, dass dieses Problem aufgrund eines „Synchronisierungsproblems“ zwischen zwei Threads aufgetreten ist.

Mutexe

Nun, da wir das Basisproblem verstanden haben, wollen wir die Lösung dafür diskutieren. Die beliebteste Art, Thread-Synchronisation zu erreichen, ist die Verwendung von Mutexes.

Ein Mutex ist eine Sperre, die wir vor der Verwendung einer gemeinsam genutzten Ressource festlegen und nach der Verwendung freigeben. Wenn die Sperre gesetzt ist, kann kein anderer Thread auf den gesperrten Codebereich zugreifen. Wir sehen also, dass selbst wenn Thread 2 geplant ist, während Thread 1 noch nicht auf die gemeinsam genutzte Ressource zugegriffen hat und der Code von Thread 1 mithilfe von Mutexes gesperrt ist, Thread 2 nicht einmal auf diesen Codebereich zugreifen kann. Dadurch wird ein synchronisierter Zugriff auf gemeinsam genutzte Ressourcen im Code gewährleistet.

Intern funktioniert es wie folgt:

  • Angenommen, ein Thread hat einen Codebereich mit Mutex gesperrt und führt diesen Codeabschnitt aus.
  • Wenn sich der Scheduler jetzt für einen Kontextwechsel entscheidet, werden alle anderen Threads, die bereit sind, dieselbe Region auszuführen, entblockt.
  • Nur einer von allen Threads würde es bis zur Ausführung schaffen, aber wenn dieser Thread versucht, denselben Codebereich auszuführen, der bereits gesperrt ist, wird er wieder schlafen gehen.
  • Der Kontextwechsel findet immer wieder statt, aber kein Thread wäre in der Lage, den gesperrten Codebereich auszuführen, bis die Mutex-Sperre darüber aufgehoben wird.
  • Die Mutex-Sperre wird nur von dem Thread aufgehoben, der sie gesperrt hat.
  • Dies stellt also sicher, dass, sobald ein Thread ein Stück Code gesperrt hat, kein anderer Thread dieselbe Region ausführen kann, bis sie von dem Thread, der sie gesperrt hat, entsperrt wird.
  • Daher stellt dieses System die Synchronisierung zwischen den Threads sicher, während es an gemeinsam genutzten Ressourcen arbeitet.

Ein Mutex wird initialisiert und dann wird eine Sperre erreicht, indem die folgenden zwei Funktionen aufgerufen werden:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);

Die erste Funktion initialisiert einen Mutex und durch die zweite Funktion kann jeder kritische Bereich im Code gesperrt werden.

Der Mutex kann entsperrt und zerstört werden, indem folgende Funktionen aufgerufen werden:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

Die erste obige Funktion hebt die Sperre auf und die zweite Funktion zerstört die Sperre, sodass sie in Zukunft nirgendwo verwendet werden kann.

Ein praktisches Beispiel

Sehen wir uns ein Stück Code an, in dem Mutexe für die Thread-Synchronisation verwendet werden

#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

pthread_t tid[2];
int counter;
pthread_mutex_t lock;

void* doSomeThing(void *arg)
{
    pthread_mutex_lock(&lock);

    unsigned long i = 0;
    counter += 1;
    printf("\n Job %d started\n", counter);

    for(i=0; i<(0xFFFFFFFF);i++);

    printf("\n Job %d finished\n", counter);

    pthread_mutex_unlock(&lock);

    return NULL;
}

int main(void)
{
    int i = 0;
    int err;

    if (pthread_mutex_init(&lock, NULL) != 0)
    {
        printf("\n mutex init failed\n");
        return 1;
    }

    while(i < 2)
    {
        err = pthread_create(&(tid[i]), NULL, &doSomeThing, NULL);
        if (err != 0)
            printf("\ncan't create thread :[%s]", strerror(err));
        i++;
    }

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_mutex_destroy(&lock);

    return 0;
}

Im Code oben:

  • Ein Mutex wird am Anfang der Hauptfunktion initialisiert.
  • Derselbe Mutex ist in der Funktion „doSomeThing()“ gesperrt, während die gemeinsam genutzte Ressource „counter“ verwendet wird
  • Am Ende der Funktion ‚doSomeThing()‘ wird derselbe Mutex entsperrt.
  • Am Ende der Hauptfunktion, wenn beide Threads fertig sind, wird der Mutex zerstört.

Wenn wir uns nun die Ausgabe ansehen, finden wir :

$ ./threads
Job 1 started
Job 1 finished
Job 2 started
Job 2 finished

Wir sehen also, dass dieses Mal die Start- und Endprotokolle beider Jobs vorhanden waren. Die Thread-Synchronisation erfolgte also durch die Verwendung von Mutex.


Linux
  1. So verwenden Sie erweitertes rsync für große Linux-Backups

  2. So verwenden Sie systemd-nspawn für die Linux-Systemwiederherstellung

  3. So verwenden Sie den Linux-Rsync-Befehl (Remote-Synchronisierung).

  4. Verwendung des Linux-Touch-Befehls + Beispiele

  5. Verwendung des Linux-Hexdump-Befehls mit praktischen Beispielen

So verwenden Sie den Grep-Befehl unter Linux + Nützliche Grep-Beispiele

So suchen Sie unter Linux nach Listening Ports (Ports in use)

So verwenden Sie den Dig-Befehl unter Linux (10 Beispiele)

So verwenden Sie den Gunzip-Befehl unter Linux mit Beispielen

So verwenden Sie den Linux-rm-Befehl mit Beispielen

So verwenden Sie den Jobs-Befehl unter Linux