Condivisione della tecnologia

Multiprocesso e multithreading Linux (8) Multithreading

2024-07-12

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

Multithreading

Definizione del filo

Un thread è un'unità di esecuzione in un processo.

Responsabile dell'esecuzione dei programmi nel processo attuale,

Esiste almeno un thread in un processo

Possono esserci più thread in un processo

Più thread condividono tutte le risorse dello stesso processo e ogni thread partecipa alla pianificazione unificata del sistema operativo

Può essere semplicemente inteso come processo = risorse di memoria + thread principale + thread figlio +...

Thread e processi

Per le attività strettamente correlate, è preferibile il multithreading durante la concorrenza. Per le attività non strettamente correlate e relativamente indipendenti, si consiglia di scegliere il multiprocesso.

  • Processo: l'unità base di allocazione delle risorse da parte del sistema operativo. È l'unità più piccola di allocazione delle risorse, l'unità di esecuzione e pianificazione del programma e l'istanza in esecuzione del programma.
  • Thread: è l'unità di base della pianificazione e dell'invio della CPU, l'unità più piccola dell'esecuzione della CPU, l'unità più piccola del flusso di esecuzione del programma e l'unità più piccola dell'esecuzione del programma.

La differenza tra thread e processi:

  • spazio di memoria
    • Più thread in un processo condividono lo stesso spazio di memoria
    • Più processi hanno uno spazio di memoria indipendente
  • Comunicazione tra processi/thread
    • Comunicazione semplice tra thread
    • La comunicazione tra processi è complessa

Risorse del thread

  • Risorse di processo condivise
    • stesso spazio di indirizzi
    • tabella dei descrittori di file
    • Come viene elaborato ciascun segnale
    • directory di lavoro corrente
    • ID utente e ID gruppo
  • Risorse uniche
    • Pila di fili
    • Ogni thread ha informazioni di contesto private
    • ID discussione
    • Registrare il valore
    • valore errato
    • Parole maschera di segnalazione e priorità di pianificazione

Comandi relativi al thread

Esistono molti comandi per visualizzare il processo nel sistema Linux, inclusi pidstat, top, ps, puoi visualizzare il processo, puoi anche visualizzare un
thread in corso

comando pidstat

Dopo che lo strumento sysstat deve essere installato sotto Ubuntu, è possibile supportare pidstat.

sudo apt install sysstat

Opzioni

-t: visualizza i thread associati al processo specificato

-p: specifica il pid del processo

Esempio

Visualizza i thread associati al processo 12345

sudo pidstat -t -p 12345

Visualizza i thread associati a tutti i processi

sudo pidstat -t

Visualizza i thread associati al processo 12345, output ogni 1 secondo

sudo pidstat -t -p 12345 1

Visualizza i thread associati a tutti i processi, emessi ogni 1 secondo

sudo pidstat -t 1

comando superiore

Utilizza il comando top per visualizzare i thread in un determinato processo. È necessario utilizzare l'opzione -H in combinazione con -p per specificare il pid.

Opzioni

-H: Visualizza le informazioni sul thread

-p: specifica il pid del processo

Esempio

Visualizza i thread associati al processo 12345

sudo top -H -p 12345

Visualizza i thread associati a tutti i processi

sudo top -H

comando ps

Utilizza il comando ps combinato con l'opzione -T per visualizzare tutti i thread in un processo

Opzioni

-T: Visualizza le informazioni sul thread

-p: specifica il pid del processo

Esempio

Visualizza i thread associati al processo 12345

sudo ps -T -p 12345

Visualizza i thread associati a tutti i processi

sudo ps -T

Scenari di concorrenza comuni

1. Modalità multiprocesso

Nella modalità multiprocesso, ogni processo è responsabile di attività diverse senza interferire tra loro. Ciascun processo viene eseguito in uno spazio di memoria diverso senza influenzarsi a vicenda.

  • vantaggio:
    • Lo spazio degli indirizzi del processo è indipendente Una volta che si verifica un'eccezione in un processo, non influenzerà gli altri processi.
  • discordanza:
    • Ogni processo deve allocare spazio di memoria indipendente. La creazione di un processo è costosa e richiede più memoria.
    • La collaborazione tra processi e la comunicazione tra processi sono più complesse
  • Scena applicabile:
    • Più attività non sono strettamente correlate, quindi è possibile utilizzare la modalità multiprocesso
    • Non esistono dipendenze tra le attività ed è possibile utilizzare la modalità multiprocesso

2. Modalità multi-thread

Nella modalità multi-threading, possono esserci più thread in un processo, che condividono lo stesso spazio di memoria, e i thread possono comunicare direttamente.

  • vantaggio:
    • La comunicazione tra i thread è semplice
    • Più thread dello stesso processo possono condividere risorse e migliorare l'utilizzo delle risorse.
  • discordanza:
    • I thread non hanno spazi di indirizzi di processo indipendenti. Dopo l'uscita del thread principale, usciranno anche gli altri thread.
    • Il cambio e la pianificazione dei thread richiedono risorse. Troppi thread consumeranno risorse di sistema.
    • La sincronizzazione tra i thread è complessa ed è necessario considerare i problemi di sicurezza dei thread
  • Scena applicabile:
    • Esistono dipendenze tra le attività ed è possibile utilizzare la modalità multi-threading
    • La comunicazione tra le attività è relativamente frequente ed è possibile utilizzare la modalità multi-threading.

Crea discussione

1. pthread_create()

pthread_create() viene utilizzato per creare un thread. Dopo che la creazione ha avuto esito positivo, il thread inizia a essere eseguito.
Dopo che pthread_create() è stato chiamato con successo, restituirà 0, altrimenti restituirà un codice di errore.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • thread: puntatore al tipo pthread_t, utilizzato per memorizzare l'ID del thread.
  • attr: attributo del thread, che può essere NULL, a indicare che viene utilizzato l'attributo predefinito.
  • start_routine: funzione di ingresso del thread.
  • arg: parametri passati alla funzione di immissione del thread.

valore di ritorno:

  • 0: creato con successo.
  • EAGAIN: risorse insufficienti, creazione del thread non riuscita.
  • EINVAL: il parametro non è valido.
  • ENOMEM: memoria insufficiente, creazione del thread non riuscita.

Avviso:

  • Una volta creato correttamente, il thread figlio verrà pianificato in modo indipendente per l'esecuzione ed eseguito contemporaneamente ad altri thread.
  • La libreria -lpthread deve essere collegata durante la compilazione.

Esempio: creare una discussione

// 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() esce dal thread

pthread_exit() viene utilizzato per uscire dal thread. Dopo che il thread ha completato l'esecuzione, pthread_exit() verrà automaticamente chiamato per uscire.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • retval: il valore restituito all'uscita del thread.
  • Dopo che la funzione thread è stata eseguita, pthread_exit() verrà automaticamente chiamato per uscire.

3. pthread_join() attende la fine del thread

pthread_join() viene utilizzato per attendere la fine del thread,
Dopo aver chiamato pthread_join(), il thread corrente verrà bloccato fino al termine del thread.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • discussione: ID discussione.
  • retval: puntatore al valore restituito dal thread, utilizzato per memorizzare il valore restituito all'uscita del thread. (puntatore secondario)

valore di ritorno:

  • 0: attendere il successo.
  • EINVAL: il parametro non è valido.
  • ESRCH: l'ID del thread non esiste.
  • EDEADLK: il thread è in uno stato di deadlock.

Esempio:

// 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

Separazione del filo

I fili sono divisi in combinabili e staccabili

  • Può essere combinato
    • Un thread combinabile può recuperare le sue risorse ed essere ucciso da altri thread; le sue risorse di memoria (come lo stack) non vengono rilasciate finché non vengono recuperate da altri thread;
    • Lo stato predefinito della creazione del thread è combinabile. La funzione pthread_join può essere chiamata da altri thread per attendere l'uscita del thread figlio e rilasciare le risorse correlate.
  • separabile
    • Non può essere riciclato o ucciso da altri thread. Le risorse del thread vengono rilasciate dal sistema quando termina.
// 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

Crea più thread

Esempio 1: creare più thread per eseguire attività diverse

// 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

Esempio 2: creare più thread per eseguire la stessa attività

// 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

Comunicazione tra thread

Altre comunicazioni tra processi si applicano anche alla comunicazione tra thread.

Il thread principale passa i parametri al thread figlio

Quando si crea un thread figlio tramite la funzione pthread_create(), il quarto parametro di pthread_create() è il parametro passato alla funzione del thread figlio.

Il sottothread passa i parametri al thread principale

Quando si esce dal thread figlio tramite la funzione pthread_exit(), è possibile passare i parametri al thread principale.

void pth_exit(void *retval);
  • 1

Quando si attende la fine del sottothread tramite la funzione pthread_join(), ottenere i parametri di ritorno del sottothread.

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

Esempio:

// 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

Discussione mutex

Discussione mutex

Mutex è un meccanismo di sincronizzazione utilizzato per controllare l'accesso alle risorse condivise.

Il vantaggio principale dei thread è la capacità di condividere informazioni attraverso variabili globali, ma questa comoda condivisione ha un costo:

È necessario garantire che più thread non modifichino la stessa variabile contemporaneamente

Un thread non leggerà le variabili modificate da altri thread Infatti, due thread non possono accedere alla sezione critica contemporaneamente.

Il principio del mutex lock

Il principio di un blocco mutex è che quando un thread tenta di entrare in un'area mutex, se l'area mutex è già occupata da altri thread, il thread verrà bloccato finché l'area mutex non verrà rilasciata.

Si tratta essenzialmente di una variabile di tipo pthread_mutex_t, che contiene un valore intero per rappresentare lo stato dell'area mutex.
Quando il valore è 1, significa che la risorsa critica corrente può competere per l'accesso e il thread che ottiene il blocco mutex può entrare nella sezione critica. In questo momento il valore è 0 e gli altri thread possono solo attendere.
Quando il valore è 0, significa che la risorsa critica corrente è occupata da altri thread e non può entrare nella sezione critica e può solo attendere.

Caratteristiche dei blocchi mutex

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
  • Il blocco mutex è una variabile di tipo pthread_mutex_t, che rappresenta un blocco mutex.
  • Se due thread accedono alla stessa variabile pthread_mutex_t, accedono allo stesso blocco mutex
  • Le variabili corrispondenti sono definite nel file di intestazione pthreadtypes.h, che è un corpo comune contenente una struttura.

Utilizzo dei blocchi mutex

Esistono due modi principali per inizializzare i blocchi mutex del thread:

inizializzazione statica

  • Definire una variabile di tipo pthread_mutex_t e inizializzarla su PTHREAD_MUTEX_INITIALIZER.
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
  • 1

inizializzazione dinamica

Inizializzazione dinamica L'inizializzazione dinamica coinvolge principalmente due funzioni: la funzione pthread_mutex_init e la funzione pthread_mutex_destroy

funzione pthread_mutex_init()

Utilizzato per inizializzare un blocco mutex, accetta due parametri: l'indirizzo del blocco mutex e gli attributi del blocco mutex.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • mutex: puntatore al tipo pthread_mutex_t, utilizzato per memorizzare l'indirizzo del blocco mutex.
  • attr: l'attributo del blocco mutex, che può essere NULL, indicando che viene utilizzato l'attributo predefinito.

valore di ritorno:

  • 0: inizializzazione riuscita.
  • In caso di errore viene restituito un codice di errore.
funzione pthread_mutex_destroy()

Utilizzato per distruggere il blocco mutex, accetta un parametro: l'indirizzo del blocco mutex.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • mutex: puntatore al tipo pthread_mutex_t, utilizzato per memorizzare l'indirizzo del blocco mutex.

valore di ritorno:

  • 0: Distruzione riuscita.
  • In caso di errore viene restituito un codice di errore.

Esempio:

// 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

Sincronizzazione dei thread

Sincronizzazione dei thread: si riferisce all'accesso ordinato alle risorse da parte dei visitatori attraverso altri meccanismi sulla base della mutua esclusione (nella maggior parte dei casi).

Variabile di condizione: un meccanismo fornito dalla libreria di thread specificatamente per la sincronizzazione dei thread.

Uno scenario applicativo tipico per la sincronizzazione dei thread è tra produttori e consumatori.

Problematiche del produttore e del consumatore

In questo modello, è diviso in thread produttore e thread consumatore e tramite questo thread viene simulato il processo di sincronizzazione di più thread.

In questo modello sono necessari i seguenti componenti:

  • Magazzino: utilizzato per immagazzinare prodotti, generalmente come risorsa condivisa
  • Thread del produttore: utilizzato per produrre prodotti
  • Thread del consumatore: utilizzato per consumare prodotti

principio:

Quando non c'è prodotto nel magazzino, il thread del consumatore deve attendere finché non c'è un prodotto prima di poterlo consumare.

Quando il magazzino è pieno di prodotti, il thread del produttore deve attendere finché il thread del consumatore non consuma i prodotti.

Esempio di implementazione di modelli produttore e consumatore basati su mutex lock

Il filo conduttore è il consumatore

n sottothread come produttori

// 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

variabile di condizione

Una variabile di condizione è un meccanismo di sincronizzazione che consente a un thread di attendere che venga soddisfatta una determinata condizione prima di continuare l'esecuzione.

Il principio di una variabile di condizione è che contiene un blocco mutex e una coda di attesa.

I blocchi mutex vengono utilizzati per proteggere le code di attesa e le variabili di condizione.

Inserisci qui la descrizione dell'immagine

Inizializzazione della variabile condizionale

La natura della variabile di condizione è di tipo 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

L'inizializzazione delle variabili condizionali è divisa in inizializzazione statica e inizializzazione dinamica.

inizializzazione statica

Per le variabili di condizione inizializzate staticamente, è necessario prima definire una variabile di tipo pthread_cond_t e quindi inizializzarla su PTHREAD_COND_INITIALIZER.

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 1

Inizializzazione dinamica pthread_cond_init()

Per inizializzare dinamicamente una variabile di condizione, è necessario prima definire una variabile di tipo pthread_cond_t, quindi chiamare la funzione pthread_cond_init per inizializzarla.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • cond: puntatore al tipo pthread_cond_t, utilizzato per memorizzare l'indirizzo della variabile di condizione.
  • attr: attributo della variabile condizione, che può essere NULL per utilizzare l'attributo predefinito.

valore di ritorno:

  • 0: inizializzazione riuscita.
  • In caso di errore viene restituito un codice di errore.

pthread_cond_destroy()

Utilizzato per distruggere le variabili di condizione, accetta un parametro: l'indirizzo della variabile di condizione.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • cond: puntatore al tipo pthread_cond_t, utilizzato per memorizzare l'indirizzo della variabile di condizione.

valore di ritorno:

  • 0: Distruzione riuscita.
  • In caso di errore viene restituito un codice di errore.

Utilizzo delle variabili condizionali

L'uso delle variabili di condizione è suddiviso in attesa e notifica

Attendi pthread_cond_wait()

La funzione di attesa pthread_cond_wait() accetta tre parametri: l'indirizzo della variabile di condizione, l'indirizzo del blocco mutex e il tempo di attesa.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • cond: puntatore al tipo pthread_cond_t, utilizzato per memorizzare l'indirizzo della variabile di condizione.
  • mutex: puntatore al tipo pthread_mutex_t, utilizzato per memorizzare l'indirizzo del blocco mutex.
  • abstime: timeout, che può essere NULL, indicando nessun timeout.

valore di ritorno:

  • 0: attendere il successo.
  • In caso di errore viene restituito un codice di errore.

Notifica pthread_cond_signal()

funzione di notifica
pthread_cond_signal() accetta un parametro: l'indirizzo della variabile di condizione.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • cond: puntatore al tipo pthread_cond_t, utilizzato per memorizzare l'indirizzo della variabile di condizione.

valore di ritorno:

  • 0: notifica riuscita.
  • In caso di errore viene restituito un codice di errore.

Notifica a tutti pthread_cond_broadcast()

Notifica tutte le funzioni
pthread_cond_broadcast() accetta un parametro: l'indirizzo della variabile di condizione.

File di intestazione della funzione:

#include <pthread.h>

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

Descrizione dei parametri:

  • cond: puntatore al tipo pthread_cond_t, utilizzato per memorizzare l'indirizzo della variabile di condizione.

valore di ritorno:

  • 0: notifica riuscita.
  • In caso di errore viene restituito un codice di errore.

Esempio di implementazione di modelli di produttore e consumatore basati su variabili condizionali

Inserisci qui la descrizione dell'immagine

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

Perché le variabili di condizione devono essere utilizzate insieme ai blocchi mutex?

Proteggi i dati condivisi:

I blocchi mutex vengono utilizzati per proteggere i dati condivisi e garantire che solo un thread possa accedere e modificare i dati contemporaneamente.

Ciò evita gare di dati e incoerenze.

Le variabili di condizione vengono utilizzate per la comunicazione tra thread per notificare ad altri thread che una determinata condizione è stata soddisfatta.

Tuttavia, l'operazione delle variabili di condizione non fornisce di per sé protezione per i dati condivisi e pertanto deve essere utilizzata insieme a un blocco mutex.

Evita falsi risvegli:

Una caratteristica delle variabili di condizione è che possono verificarsi risvegli spuri.

Cioè, il thread viene risvegliato senza notifica esplicita. Per evitare operazioni errate causate da questa situazione,

Il thread deve ricontrollare se la condizione è effettivamente soddisfatta dopo il risveglio.

L'uso di un mutex garantisce che i dati condivisi non vengano modificati da altri thread durante il controllo delle condizioni, evitando così errori causati da risvegli spuri.

Assicurati che la notifica sia corretta:

Quando un thread invia una notifica ad altri thread tramite variabili di condizione, deve assicurarsi che i dati condivisi siano stati aggiornati prima della notifica.

Un mutex garantisce ciò, assicurando che tutte le operazioni di aggiornamento dei dati siano state completate prima di rilasciare il blocco.

Allo stesso modo, il thread che riceve la notifica deve mantenere il mutex prima di verificare la condizione per garantire che i dati siano stabili quando la condizione viene verificata.

Implementa modelli di sincronizzazione complessi:
La combinazione di blocchi mutex con variabili di condizione può implementare modelli di sincronizzazione più complessi, come problemi produttore-consumatore, problemi lettore-scrittore, ecc. I mutex proteggono i dati condivisi e le variabili di condizione vengono utilizzate per il coordinamento e la comunicazione tra i thread.

// 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