Technologieaustausch

Linux-Multiprozess und Multithreading (8) Multithreading

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Multithreading

Thread-Definition

Ein Thread ist eine Ausführungseinheit in einem Prozess.

Verantwortlich für die Ausführung von Programmen im aktuellen Prozess,

Es gibt mindestens einen Thread in einem Prozess

In einem Prozess können mehrere Threads vorhanden sein

Mehrere Threads teilen sich alle Ressourcen desselben Prozesses und jeder Thread nimmt an der einheitlichen Planung des Betriebssystems teil

Es kann einfach als Prozess = Speicherressourcen + Hauptthread + untergeordneter Thread +… verstanden werden.

Threads und Prozesse

Für Aufgaben, die eng miteinander verbunden sind, wird Multithreading während der Parallelität bevorzugt. Für Aufgaben, die nicht eng miteinander verbunden und relativ unabhängig sind, wird empfohlen, Multiprozess zu wählen.

  • Prozess: Die Grundeinheit der Ressourcenzuweisung durch das Betriebssystem. Dies ist die kleinste Einheit der Ressourcenzuweisung, die Ausführungs- und Planungseinheit des Programms und die laufende Instanz des Programms.
  • Thread: Es handelt sich um die Grundeinheit der CPU-Planung und -Verteilung, die kleinste Einheit der CPU-Ausführung, die kleinste Einheit des Programmausführungsflusses und die kleinste Einheit der Programmausführung.

Der Unterschied zwischen Threads und Prozessen:

  • Speicherplatz
    • Mehrere Threads in einem Prozess teilen sich denselben Speicherplatz
    • Mehrere Prozesse verfügen über unabhängigen Speicherplatz
  • Kommunikation zwischen Prozessen/Threads
    • Einfache Kommunikation zwischen Threads
    • Die Kommunikation zwischen Prozessen ist komplex

Thread-Ressourcen

  • Gemeinsam genutzte Prozessressourcen
    • gleichen Adressraum
    • Dateideskriptortabelle
    • Wie jedes Signal verarbeitet wird
    • aktuelles Arbeitsverzeichnis
    • Benutzer-ID und Gruppen-ID
  • Einzigartige Ressourcen
    • Thread-Stapel
    • Jeder Thread verfügt über private Kontextinformationen
    • Thread-ID
    • Wert registrieren
    • Fehlerwert
    • Signalmaskenwörter und Planungsprioritäten

Threadbezogene Befehle

Es gibt viele Befehle zum Anzeigen des Prozesses im Linux-System, einschließlich pidstat, top, ps, Sie können den Prozess anzeigen, Sie können auch a anzeigen
Thread in Bearbeitung

pidstat-Befehl

Nachdem das Sysstat-Tool unter Ubuntu installiert werden muss, kann PIDSTAT unterstützt werden.

sudo apt installiere sysstat

Optionen

-t: Zeigt die Threads an, die dem angegebenen Prozess zugeordnet sind

-p: Geben Sie die Prozess-PID an

Beispiel

Sehen Sie sich die Threads an, die dem Prozess 12345 zugeordnet sind

sudo pidstat -t -p 12345

Zeigen Sie Threads an, die allen Prozessen zugeordnet sind

sudo pidstat -t

Zeigen Sie die Threads an, die dem Prozess 12345 zugeordnet sind und alle 1 Sekunde ausgegeben werden

sudo pidstat -t -p 12345 1

Zeigen Sie die Threads an, die allen Prozessen zugeordnet sind, und geben Sie sie alle 1 Sekunde aus

sudo pidstat -t 1

oberster Befehl

Verwenden Sie den Befehl top, um die Threads unter einem bestimmten Prozess anzuzeigen. Sie müssen die Option -H in Kombination mit -p verwenden, um die PID anzugeben.

Optionen

-H: Thread-Informationen anzeigen

-p: Geben Sie die Prozess-PID an

Beispiel

Sehen Sie sich die Threads an, die dem Prozess 12345 zugeordnet sind

sudo top -H -p 12345

Zeigen Sie Threads an, die allen Prozessen zugeordnet sind

sudo top -H

ps-Befehl

Verwenden Sie den Befehl ps in Kombination mit der Option -T, um alle Threads unter einem Prozess anzuzeigen

Optionen

-T: Thread-Informationen anzeigen

-p: Geben Sie die Prozess-PID an

Beispiel

Sehen Sie sich die Threads an, die dem Prozess 12345 zugeordnet sind

sudo ps -T -p 12345

Zeigen Sie Threads an, die allen Prozessen zugeordnet sind

sudo ps -T

Häufige Parallelitätsszenarien

1. Multiprozessmodus

Im Multiprozessmodus ist jeder Prozess für unterschiedliche Aufgaben verantwortlich, ohne sich gegenseitig zu beeinträchtigen. Jeder Prozess läuft in einem anderen Speicherbereich, ohne sich gegenseitig zu beeinflussen.

  • Vorteil:
    • Der Adressraum des Prozesses ist unabhängig. Sobald in einem Prozess eine Ausnahme auftritt, hat dies keine Auswirkungen auf andere Prozesse.
  • Mangel:
    • Jeder Prozess muss unabhängigen Speicherplatz zuweisen. Das Erstellen eines Prozesses ist teuer und beansprucht mehr Speicher.
    • Die prozessübergreifende Zusammenarbeit und die prozessübergreifende Kommunikation sind komplexer
  • Anwendbare Szene:
    • Da mehrere Aufgaben nicht sehr eng miteinander verbunden sind, kann der Multiprozessmodus verwendet werden
    • Es gibt keine Abhängigkeiten zwischen Aufgaben und der Multiprozessmodus kann verwendet werden

2. Multithread-Modus

Im Multithreading-Modus kann es in einem Prozess mehrere Threads geben, die sich denselben Speicherplatz teilen, und Threads können direkt kommunizieren.

  • Vorteil:
    • Die Kommunikation zwischen Threads ist einfach
    • Mehrere Threads desselben Prozesses können Ressourcen gemeinsam nutzen und die Ressourcennutzung verbessern.
  • Mangel:
    • Threads haben keine unabhängigen Prozessadressräume. Nachdem der Hauptthread beendet wurde, werden auch andere Threads beendet.
    • Thread-Wechsel und -Planung erfordern Ressourcen. Zu viele Threads verbrauchen Systemressourcen.
    • Die Synchronisierung zwischen Threads ist komplex und Thread-Sicherheitsaspekte müssen berücksichtigt werden
  • Anwendbare Szene:
    • Es bestehen Abhängigkeiten zwischen Aufgaben und es kann der Multithreading-Modus verwendet werden
    • Die Kommunikation zwischen Aufgaben ist relativ häufig und der Multithreading-Modus kann verwendet werden.

Erstelle einen Thread

1. pthread_create()

Mit pthread_create() wird ein Thread erstellt. Nach erfolgreicher Erstellung wird der Thread ausgeführt.
Nachdem pthread_create() erfolgreich aufgerufen wurde, wird 0 zurückgegeben, andernfalls wird ein Fehlercode zurückgegeben.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • 1
  • 2
  • 3
  • 4

Parameterbeschreibung:

  • Thread: Zeiger auf den Typ pthread_t, der zum Speichern der Thread-ID verwendet wird.
  • attr: Thread-Attribut, das NULL sein kann, was angibt, dass das Standardattribut verwendet wird.
  • start_routine: Einstiegsfunktion des Threads.
  • arg: Parameter, die an die Thread-Eingabefunktion übergeben werden.

Rückgabewert:

  • 0: Erfolgreich erstellt.
  • EAGAIN: Unzureichende Ressourcen, Thread-Erstellung fehlgeschlagen.
  • EINVAL: Der Parameter ist ungültig.
  • ENOMEM: Nicht genügend Speicher, Thread-Erstellung fehlgeschlagen.

Beachten:

  • Sobald der untergeordnete Thread erfolgreich erstellt wurde, wird seine Ausführung unabhängig geplant und gleichzeitig mit anderen Threads ausgeführt.
  • Die -lpthread-Bibliothek muss beim Kompilieren verknüpft werden.

Beispiel: Erstellen Sie einen Thread

// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("%sn",(char *)arg);
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //@param tid 线程ID
    //@param attr 线程属性
    //@param start_routine 线程函数
    //@param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }
    sleep(1); // 等待线程执行完毕
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

2. pthread_exit() verlässt den Thread

pthread_exit() wird zum Beenden des Threads verwendet. Nachdem der Thread die Ausführung abgeschlossen hat, wird pthread_exit() automatisch zum Beenden aufgerufen.

Funktions-Header-Datei:

#include <pthread.h>

void pthread_exit(void *retval);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • retval: Der Wert, der zurückgegeben wird, wenn der Thread beendet wird.
  • Nachdem die Thread-Funktion ausgeführt wurde, wird pthread_exit () automatisch zum Beenden aufgerufen.

3. pthread_join() wartet auf das Ende des Threads

pthread_join() wird verwendet, um auf das Ende des Threads zu warten.
Nach dem Aufruf von pthread_join () wird der aktuelle Thread blockiert, bis der Thread endet.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • Thread: Thread-ID.
  • retval: Zeiger auf den Thread-Rückgabewert, der zum Speichern des beim Beenden des Threads zurückgegebenen Werts verwendet wird. (sekundärer Zeiger)

Rückgabewert:

  • 0: Auf Erfolg warten.
  • EINVAL: Der Parameter ist ungültig.
  • ESRCH: Thread-ID existiert nicht.
  • EDEADLK: Der Thread befindet sich in einem Deadlock-Zustand.

Beispiel:

// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid, NULL);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
等待线程结束...
Hello, World!
  • 1
  • 2

Fadentrennung

Die Fäden sind in kombinierbare und abnehmbare unterteilt

  • Kombinierbar
    • Ein kombinierbarer Thread kann seine Ressourcen zurückfordern und von anderen Threads getötet werden; seine Speicherressourcen (z. B. der Stapel) werden erst freigegeben, wenn er von anderen Threads zurückgefordert wird.
    • Der Standardstatus der Thread-Erstellung ist kombinierbar. Die Funktion pthread_join kann von anderen Threads aufgerufen werden, um auf das Beenden des untergeordneten Threads zu warten und zugehörige Ressourcen freizugeben.
  • trennbar
    • Es kann nicht von anderen Threads recycelt oder beendet werden. Die Ressourcen des Threads werden vom System freigegeben, wenn er beendet wird.
// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    //pthread_join(tid, NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行

    //线程分离
    pthread_detach(tid); //! 分离线程,不用等待线程结束后才退出程序,该线程的资源在它终止时由系统来释放。

    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

Erstellen Sie mehrere Threads

Beispiel 1: Erstellen Sie mehrere Threads, um verschiedene Aufgaben auszuführen

// todo : 创建多个线程,执行不同的任务
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello_A(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}
// 线程函数
//@param arg 线程函数参数
void * print_hello_B(void *arg) {
    sleep(2); // 休眠2秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}


int main() {
    pthread_t tidA; //? 存储线程ID  typedef unsigned long int pthread_t;
    pthread_t tidB;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int retA = pthread_create(&tidA, NULL,print_hello_A, "A_ Hello, World!");
    if (retA!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    int retB = pthread_create(&tidB, NULL,print_hello_B, "B_ Hello, World!");
    if (retB!= 0){
        printf("pthread_create error!n");
        return 1;
    }
    
    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tidA, NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tidB, NULL);
    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

Beispiel 2: Erstellen Sie mehrere Threads, um dieselbe Aufgabe auszuführen

// todo : 创建多个线程,执行相同任务
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
//? 两个线程执行相同任务,对函数中的值修改了,会不会影响到其他线程的执行?
//! 在多线程编程中,如果多个线程执行相同的任务并且对共享资源进行修改,可能会影响到其他线程的执行。
//! 这是因为多个线程共享相同的内存空间,对共享资源的修改可能会导致竞态条件(race condition),
//! 从而导致不可预测的行为。
//! print_hello函数中的变量i是局部变量,每个线程都会有自己的i副本,因此对i的修改不会影响到其他线程。
//! 但是,如果涉及到共享资源(例如全局变量或静态变量),就需要考虑线程同步的问题,以避免竞态条件。


//*局部变量:每个线程都有自己的栈空间,因此局部变量是线程私有的,不会影响到其他线程。
//*共享资源:如果多个线程访问和修改同一个全局变量或静态变量,就需要使用同步机制(如互斥锁、信号量等)来确保线程安全。
//Linux:在Linux系统中,默认的线程栈大小通常是8MB。可以使用ulimit -s命令查看和修改当前用户的线程栈大小。例如,ulimit -s 1024将线程栈大小设置为1MB。
//Windows:在Windows系统中,默认的线程栈大小是1MB。可以通过编译器选项或在创建线程时指定栈大小来修改。

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {

    for (char i = 'a'; i < 'z' ; ++i) {
        printf("%cn", i);
        sleep(1); // 休眠1秒
    }
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid[2]={0}; //? 存储线程ID的数组  typedef unsigned long int pthread_t;


    for (int i = 0; i < 2; ++i) {
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,print_hello, NULL);
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0], NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1], NULL);


    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

Kommunikation zwischen Threads

Andere Kommunikationen zwischen Prozessen gelten auch für die Kommunikation zwischen Threads.

Der Hauptthread übergibt Parameter an den untergeordneten Thread

Beim Erstellen eines untergeordneten Threads über die Funktion pthread_create() ist der vierte Parameter von pthread_create() der Parameter, der an die Funktion des untergeordneten Threads übergeben wird.

Der Subthread übergibt Parameter an den Hauptthread

Beim Verlassen des untergeordneten Threads über die Funktion pthread_exit() können Sie Parameter an den Hauptthread übergeben.

void pth_exit(void *retval);
  • 1

Wenn Sie über die Funktion pthread_join () auf das Ende des Sub-Threads warten, erhalten Sie die Rückgabeparameter des Sub-Threads.

int pthread_join (pthread_t __th, void **__thread_return);
//二级指针获取到了pthread_exit()函数参数指针的指向地址,通过该地址可以获取到子线程的返回参数。
  • 1
  • 2

Beispiel:

// todo : 线程直接通讯,子线程向父线程传参
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("子线程开始,结束之时传递参数100的地址n");

    sleep(1); // 休眠1秒
    //! int num=100;//局部变量,函数结束释放内存
    static int num=100;//* 静态局部变量,函数结束不释放内存,延长生命周期
    pthread_exit(&num); // 线程退出
}



int main() {
    pthread_t tid; //? 存储线程ID  typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int retA = pthread_create(&tid, NULL,print_hello, NULL);
    if (retA!= 0){
        printf("pthread_create error!n");
        return 1;
    }



    printf("等待线程结束...n");
    void* num;//获取子进程传递的参数,num指向了子进程传递的参数
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid, (void **)&num);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    printf("子线程结束,传递的参数为%dn",*(int*)num);
    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

Thread-Mutex

Thread-Mutex

Mutex ist ein Synchronisationsmechanismus, der zur Steuerung des Zugriffs auf gemeinsam genutzte Ressourcen verwendet wird.

Der Hauptvorteil von Threads ist die Möglichkeit, Informationen über globale Variablen auszutauschen, aber dieser bequeme Austausch hat seinen Preis:

Es muss sichergestellt werden, dass nicht mehrere Threads gleichzeitig dieselbe Variable ändern

Ein Thread liest keine Variablen, die von anderen Threads geändert werden. Tatsächlich können nicht zwei Threads gleichzeitig auf den kritischen Abschnitt zugreifen.

Das Prinzip der Mutex-Sperre

Das Prinzip einer Mutex-Sperre besteht darin, dass, wenn ein Thread versucht, in einen Mutex-Bereich einzudringen, der Thread blockiert wird, bis der Mutex-Bereich freigegeben wird, wenn der Mutex-Bereich bereits von anderen Threads belegt ist.

Es handelt sich im Wesentlichen um eine Variable vom Typ pthread_mutex_t, die einen ganzzahligen Wert enthält, der den Status des Mutex-Bereichs darstellt.
Wenn der Wert 1 ist, bedeutet dies, dass die aktuelle kritische Ressource um Zugriff konkurrieren kann und der Thread, der die Mutex-Sperre erhält, den kritischen Abschnitt betreten kann. Zu diesem Zeitpunkt ist der Wert 0 und andere Threads können nur warten.
Wenn der Wert 0 ist, bedeutet dies, dass die aktuelle kritische Ressource von anderen Threads belegt ist und nicht in den kritischen Abschnitt gelangen kann, sondern nur warten kann.

Eigenschaften von Mutex-Sperren

typedef union
{
  struct __pthread_mutex_s __data; // 互斥锁的结构体
  char __size[__SIZEOF_PTHREAD_MUTEX_T];// 互斥锁的大小
  long int __align;// 互斥锁的对齐
} pthread_mutex_t;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • Die Mutex-Sperre ist eine Variable vom Typ pthread_mutex_t, die eine Mutex-Sperre darstellt.
  • Wenn zwei Threads auf dieselbe pthread_mutex_t-Variable zugreifen, greifen sie auf dieselbe Mutex-Sperre zu
  • Die entsprechenden Variablen sind in der Header-Datei pthreadtypes.h definiert, bei der es sich um einen gemeinsamen Textkörper handelt, der eine Struktur enthält.

Verwendung von Mutex-Sperren

Es gibt zwei Möglichkeiten, Thread-Mutex-Sperren zu initialisieren:

statische Initialisierung

  • Definieren Sie eine Variable vom Typ pthread_mutex_t und initialisieren Sie sie mit PTHREAD_MUTEX_INITIALIZER.
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
  • 1

dynamische Initialisierung

Dynamische Initialisierung Die dynamische Initialisierung umfasst hauptsächlich zwei Funktionen: die Funktion pthread_mutex_init und die Funktion pthread_mutex_destroy

pthread_mutex_init()-Funktion

Es wird zum Initialisieren einer Mutex-Sperre verwendet und akzeptiert zwei Parameter: die Adresse der Mutex-Sperre und die Attribute der Mutex-Sperre.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • mutex: Zeiger auf den Typ pthread_mutex_t, der zum Speichern der Adresse der Mutex-Sperre verwendet wird.
  • attr: Das Attribut der Mutex-Sperre, das NULL sein kann, was angibt, dass das Standardattribut verwendet wird.

Rückgabewert:

  • 0: Initialisierung erfolgreich.
  • Bei einem Fehler wird ein Fehlercode zurückgegeben.
pthread_mutex_destroy()-Funktion

Wird zur Zerstörung der Mutex-Sperre verwendet und akzeptiert einen Parameter: die Adresse der Mutex-Sperre.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • mutex: Zeiger auf den Typ pthread_mutex_t, der zum Speichern der Adresse der Mutex-Sperre verwendet wird.

Rückgabewert:

  • 0: Zerstörung erfolgreich.
  • Bei einem Fehler wird ein Fehlercode zurückgegeben.

Beispiel:

// todo :  互斥锁;创建两个线程,分别对全局变量进⾏ +1 操作
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>


static int global = 0;// 全局变量

//静态初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 互斥锁
//动态初始化互斥锁
pthread_mutex_t mut;// 互斥锁

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("子线程开始n");

    int loops = *(int *)arg;
    int i,tmp = 0;
    for (i = 0;i < loops;i++){
        pthread_mutex_lock(&mut);// 加锁
        printf("子线程%d,global=%dn",i,global);
        tmp = global;
        tmp++;
        global = tmp;
        pthread_mutex_unlock(&mut);// 解锁
    }
    printf("子线程结束n");
    pthread_exit(NULL); // 线程退出
}



int main() {

    // 动态初始化互斥锁
    //* @param mutex 互斥锁
    //* @param attr 互斥锁属性 NULL 是默认属性
    int r= pthread_mutex_init(&mut,NULL);
    if (r!= 0){
        printf("pthread_mutex_init error!n");
        return 1;
    }

    pthread_t tid[2]={0}; //? 存储线程ID  typedef unsigned long int pthread_t;
    int arg=20;
    for (int i = 0; i < 2; i++){
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,print_hello, &arg);
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }




    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0],NULL );//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1],NULL );

    printf("%dn",global);
    printf("主线程结束n");

    // 销毁动态创建的互斥锁
    //* @param mutex 互斥锁
    pthread_mutex_destroy(&mut);// 销毁互斥锁

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

Thread-Synchronisation

Thread-Synchronisation: bezieht sich auf den geordneten Zugriff von Besuchern auf Ressourcen über andere Mechanismen auf der Grundlage gegenseitigen Ausschlusses (in den meisten Fällen).

Bedingungsvariable: Ein Mechanismus, der von der Thread-Bibliothek speziell für die Thread-Synchronisierung bereitgestellt wird.

Ein typisches Anwendungsszenario für die Thread-Synchronisation ist zwischen Produzenten und Konsumenten.

Produzenten- und Verbraucherfragen

In diesem Modell ist es in Produzenten-Thread und Verbraucher-Thread unterteilt, und der Prozess der Synchronisation mehrerer Threads wird durch diesen Thread simuliert.

In diesem Modell werden folgende Komponenten benötigt:

  • Lager: Wird zur Lagerung von Produkten verwendet, im Allgemeinen als gemeinsam genutzte Ressource
  • Hersteller-Thread: Wird zur Herstellung von Produkten verwendet
  • Verbraucherthread: Wird für den Konsum von Produkten verwendet

Prinzip:

Wenn sich kein Produkt im Lager befindet, muss der Verbraucherthread warten, bis ein Produkt vorhanden ist, bevor er es verbrauchen kann.

Wenn das Lager mit Produkten gefüllt ist, muss der Produzenten-Thread warten, bis der Verbraucher-Thread die Produkte verbraucht.

Beispiel für die Implementierung von Produzenten- und Verbrauchermodellen basierend auf Mutex-Sperren

Der Hauptthread ist der Verbraucher

n Unterthreads als Produzenten

// todo :  基于互斥锁实现⽣产者与消费者模型主线程为消费者,n 个⼦线程作为⽣产者
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int n = 0; // 产品数量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 互斥锁

//生产者执行函数
void * dofunc(void *arg) {
    int arg1 = *(int*)arg;
    for (int i = 0; i <arg1; i++) {
        //获取互斥锁
        pthread_mutex_lock(&mutex);
        //生产产品
        printf("生产者%ld生产了%d个产品n",pthread_self(),++n);//! pthread_self()返回当前线程ID
        //释放互斥锁
        pthread_mutex_unlock(&mutex);
        //休眠1秒
        sleep(1);
    }
    pthread_exit(NULL);
}


int main() {
    pthread_t tid[4]={0}; //? 存储线程ID  typedef unsigned long int pthread_t;
    int arr[4]={1,2,3,4};
    for (int i = 0; i < 4; i++) {
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,dofunc,&arr[i] );
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }
    //消费者执行

    for (int i = 0;i<10;i++) {
        //获取互斥锁
        pthread_mutex_lock(&mutex);
        while (n > 0){
            //消费产品
            printf("消费者%ld消费了1个产品:%dn",pthread_self(),n--);
        }
        //释放互斥锁
        pthread_mutex_unlock(&mutex);
        //休眠1秒
        sleep(1);
    }


    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0],NULL );//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1],NULL );
    pthread_join(tid[2],NULL );
    pthread_join(tid[3],NULL );

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

Bedingungsvariable

Eine Bedingungsvariable ist ein Synchronisationsmechanismus, der es einem Thread ermöglicht, auf die Erfüllung einer bestimmten Bedingung zu warten, bevor er weiter ausgeführt wird.

Das Prinzip einer Bedingungsvariablen besteht darin, dass sie eine Mutex-Sperre und eine Warteschlange enthält.

Mutex-Sperren werden zum Schutz von Warteschlangen und Bedingungsvariablen verwendet.

Fügen Sie hier eine Bildbeschreibung ein

Initialisierung der Bedingungsvariablen

Die Art der Bedingungsvariablen ist vom Typ pthread_cond_t

其他线程可以阻塞在这个条件变量上, 或者唤
醒阻塞在这个条件变量上的线程
typedef union
{
  struct __pthread_cond_s __data;
  char __size[__SIZEOF_PTHREAD_COND_T];
  __extension__ long long int __align;
} pthread_cond_t;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Die Initialisierung von Bedingungsvariablen ist in statische Initialisierung und dynamische Initialisierung unterteilt.

statische Initialisierung

Für statisch initialisierte Bedingungsvariablen müssen Sie zunächst eine Variable vom Typ pthread_cond_t definieren und diese dann mit PTHREAD_COND_INITIALIZER initialisieren.

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 1

Dynamische Initialisierung pthread_cond_init()

Um eine Bedingungsvariable dynamisch zu initialisieren, müssen Sie zunächst eine Variable vom Typ pthread_cond_t definieren und dann die Funktion pthread_cond_init aufrufen, um sie zu initialisieren.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • cond: Zeiger auf den Typ pthread_cond_t, der zum Speichern der Adresse der Bedingungsvariablen verwendet wird.
  • attr: Attribut der Bedingungsvariablen, das NULL sein kann, um das Standardattribut zu verwenden.

Rückgabewert:

  • 0: Initialisierung erfolgreich.
  • Bei einem Fehler wird ein Fehlercode zurückgegeben.

pthread_cond_destroy()

Es wird zum Zerstören von Bedingungsvariablen verwendet und akzeptiert einen Parameter: die Adresse der Bedingungsvariablen.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • cond: Zeiger auf den Typ pthread_cond_t, der zum Speichern der Adresse der Bedingungsvariablen verwendet wird.

Rückgabewert:

  • 0: Zerstörung erfolgreich.
  • Bei einem Fehler wird ein Fehlercode zurückgegeben.

Verwendung von Bedingungsvariablen

Die Verwendung von Bedingungsvariablen ist in Warten und Benachrichtigung unterteilt

Warten Sie auf pthread_cond_wait()

Die Wartefunktion pthread_cond_wait() akzeptiert drei Parameter: die Adresse der Bedingungsvariablen, die Adresse der Mutex-Sperre und die Wartezeit.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • cond: Zeiger auf den Typ pthread_cond_t, der zum Speichern der Adresse der Bedingungsvariablen verwendet wird.
  • mutex: Zeiger auf den Typ pthread_mutex_t, der zum Speichern der Adresse der Mutex-Sperre verwendet wird.
  • abstime: Zeitüberschreitung, die NULL sein kann, was darauf hinweist, dass keine Zeitüberschreitung vorliegt.

Rückgabewert:

  • 0: Auf Erfolg warten.
  • Bei einem Fehler wird ein Fehlercode zurückgegeben.

Benachrichtigen Sie pthread_cond_signal()

Benachrichtigungsfunktion
pthread_cond_signal() akzeptiert einen Parameter: die Adresse der Bedingungsvariablen.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • cond: Zeiger auf den Typ pthread_cond_t, der zum Speichern der Adresse der Bedingungsvariablen verwendet wird.

Rückgabewert:

  • 0: Benachrichtigung erfolgreich.
  • Bei einem Fehler wird ein Fehlercode zurückgegeben.

Benachrichtigen Sie alle pthread_cond_broadcast()

Alle Funktionen benachrichtigen
pthread_cond_broadcast() akzeptiert einen Parameter: die Adresse der Bedingungsvariablen.

Funktions-Header-Datei:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
  • 1
  • 2
  • 3

Parameterbeschreibung:

  • cond: Zeiger auf den Typ pthread_cond_t, der zum Speichern der Adresse der Bedingungsvariablen verwendet wird.

Rückgabewert:

  • 0: Benachrichtigung erfolgreich.
  • Bei einem Fehler wird ein Fehlercode zurückgegeben.

Beispiel für die Implementierung von Produzenten- und Konsumentenmodellen basierend auf bedingten Variablen

Fügen Sie hier eine Bildbeschreibung ein

step 1 : 消费者线程判断消费条件是否满足 (仓库是否有产品),如果有产品可以消费,则可以正
常消费产品,然后解锁
step 2 : 当条件不能满足时 (仓库产品数量为 0),则调用 pthread_cond_wait 函数, 这个函数
            具体做的事情如下:
            在线程睡眠之前,对互斥锁解锁
            让线程进⼊到睡眠状态
            等待条件变量收到信号时 唤醒,该函数重新竞争锁,并获取锁后,函数返回 
step 3 :重新判断条件是否满足, 如果不满足,则继续调用 pthread_cond_wait 函数
step 4 : 唤醒后,从 pthread_cond_wait 返回,消费条件满足,则正常消费产品
step 5 : 释放锁,整个过程结束
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Warum müssen Bedingungsvariablen in Verbindung mit Mutex-Sperren verwendet werden?

Geteilte Daten schützen:

Mutex-Sperren werden verwendet, um gemeinsam genutzte Daten zu schützen und sicherzustellen, dass nur ein Thread gleichzeitig auf die Daten zugreifen und diese ändern kann.

Dadurch werden Datenwettläufe und Inkonsistenzen vermieden.

Bedingungsvariablen werden für die Kommunikation zwischen Threads verwendet, um andere Threads darüber zu informieren, dass eine bestimmte Bedingung erfüllt ist.

Allerdings bietet die Operation von Bedingungsvariablen selbst keinen Schutz für gemeinsam genutzte Daten und muss daher in Verbindung mit einer Mutex-Sperre verwendet werden.

Vermeiden Sie falsches Aufwachen:

Ein Merkmal von Bedingungsvariablen besteht darin, dass es zu einem falschen Aufwecken kommen kann.

Das heißt, der Thread wird ohne explizite Benachrichtigung aktiviert. Um durch diese Situation verursachte Fehlbedienungen zu vermeiden,

Der Thread muss nach dem Aufwachen erneut prüfen, ob die Bedingung tatsächlich erfüllt ist.

Durch die Verwendung eines Mutex wird sichergestellt, dass gemeinsam genutzte Daten bei der Überprüfung der Bedingungen nicht von anderen Threads geändert werden, wodurch Fehler durch falsches Aufwecken vermieden werden.

Stellen Sie sicher, dass die Benachrichtigung korrekt ist:

Wenn ein Thread andere Threads über Bedingungsvariablen benachrichtigt, muss er vor der Benachrichtigung sicherstellen, dass die gemeinsam genutzten Daten aktualisiert wurden.

Ein Mutex garantiert dies und stellt sicher, dass alle Datenaktualisierungsvorgänge abgeschlossen sind, bevor die Sperre aufgehoben wird.

Ebenso muss der Thread, der die Benachrichtigung empfängt, den Mutex halten, bevor er die Bedingung überprüft, um sicherzustellen, dass die Daten stabil sind, wenn die Bedingung überprüft wird.

Implementieren Sie komplexe Synchronisationsmuster:
Durch die Kombination von Mutex-Sperren mit Bedingungsvariablen können komplexere Synchronisationsmuster implementiert werden, z. B. Producer-Consumer-Probleme, Reader-Writer-Probleme usw. Mutexe schützen gemeinsam genutzte Daten und Bedingungsvariablen werden zur Koordination und Kommunikation zwischen Threads verwendet.

// todo :  条件变量
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdlib.h>


static int number = 0;// 产品数量
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;// 互斥锁
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;// 条件变量

// 线程函数
//@param arg 线程函数参数
void * thread_handler(void *arg) {
    int cnt = atoi((char *)arg);// 获取线程参数
    int i,tmp;// 临时变量
    for(i = 0;i < cnt;i++){// 生产产品
        pthread_mutex_lock(&mtx);// 上锁
        printf("线程 [%ld] ⽣产⼀个产品,产品数量为:%dn",pthread_self(),++number);
        pthread_mutex_unlock(&mtx);// 解锁

        //! 唤醒cond阻塞的线程
        //! @param cond 条件变量
        //pthread_cond_signal(&cond);//! 只能唤醒一个线程,如果有多个线程在等待,则只有一个线程会被唤醒
        //唤醒所有线程
        pthread_cond_broadcast(&cond);
    }
    pthread_exit((void *)0);// 线程退出
}



int main(int argc,char *argv[]) {

    pthread_t tid[argc-1];// 线程ID
    int i;
    int err;
    int total_of_produce = 0;// 总共生产的产品数量
    int total_of_consume = 0;// 总共消费的产品数量
    bool done = false;// 是否完成生产
    //循环创建线程
    for (i = 1;i < argc;i++){
        total_of_produce += atoi(argv[i]);// 计算总共需要生产的产品数量
        // 创建线程
        err = pthread_create(&tid[i-1],NULL,thread_handler,(void *)argv[i]);
        if (err != 0){
            perror("[ERROR] pthread_create(): ");
            exit(EXIT_FAILURE);
        }
    }
    //消费者
    for (;;){
        //*先获取锁,再进行条件变量的等待
        pthread_mutex_lock(&mtx);// 上锁

        //*while循环来判断条件,避免虚假唤醒
        while(number == 0) {// 等待生产者生产产品
            //! 等待条件变量
            //! @param cond 条件变量
            //! @param mtx 互斥锁
            //! 函数中会释放互斥锁,并阻塞线程,
            //! 直到条件变量被唤醒,再重新竞争互斥锁,获取互斥锁并继续执行
            pthread_cond_wait(&cond, &mtx);
        }
        while(number > 0){
            total_of_consume++;// 总共消费的产品数量
            printf("消费⼀个产品,产品数量为:%dn",--number);// 消费产品
            done = total_of_consume >= total_of_produce;// 是否完成生产
        }
        pthread_mutex_unlock(&mtx);// 解锁

        if (done)// 是否完成生产
            break;
    }

    // 等待线程退出
    for(i = 0;i < argc-1;i++){
        pthread_join(tid[i],NULL);
    }

    return 0;

}
    //循环创建线程
    for (i = 1;i < argc;i++){
        total_of_produce += atoi(argv[i]);// 计算总共需要生产的产品数量
        // 创建线程
        err = pthread_create(&tid[i-1],NULL,thread_handler,(void *)argv[i]);
        if (err != 0){
            perror("[ERROR] pthread_create(): ");
            exit(EXIT_FAILURE);
        }
    }
    //消费者
    for (;;){
        //*先获取锁,再进行条件变量的等待
        pthread_mutex_lock(&mtx);// 上锁

        //*while循环来判断条件,避免虚假唤醒
        while(number == 0) {// 等待生产者生产产品
            //! 等待条件变量
            //! @param cond 条件变量
            //! @param mtx 互斥锁
            //! 函数中会释放互斥锁,并阻塞线程,
            //! 直到条件变量被唤醒,再重新竞争互斥锁,获取互斥锁并继续执行
            pthread_cond_wait(&cond, &mtx);
        }
        while(number > 0){
            total_of_consume++;// 总共消费的产品数量
            printf("消费⼀个产品,产品数量为:%dn",--number);// 消费产品
            done = total_of_consume >= total_of_produce;// 是否完成生产
        }
        pthread_mutex_unlock(&mtx);// 解锁

        if (done)// 是否完成生产
            break;
    }

    // 等待线程退出
    for(i = 0;i < argc-1;i++){
        pthread_join(tid[i],NULL);
    }

    return 0;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127