Обмен технологиями

Многопроцессность и многопоточность Linux (8)

2024-07-12

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

Многопоточность

Определение резьбы

Поток — это исполнительная единица процесса.

Отвечает за выполнение программ в текущем процессе,

В процессе есть хотя бы один поток

В процессе может быть несколько потоков

Несколько потоков совместно используют все ресурсы одного процесса, и каждый поток участвует в едином планировании операционной системы.

Его можно просто понять как процесс = ресурсы памяти + основной поток + дочерний поток +…

Потоки и процессы

Для тесно связанных задач во время параллельного выполнения предпочтительна многопоточность. Для задач, которые не тесно связаны и относительно независимы, рекомендуется выбирать многопроцессность.

  • Процесс: основная единица выделения ресурсов операционной системой. Это наименьшая единица выделения ресурсов, единица выполнения и планирования программы и выполняющийся экземпляр программы.
  • Тема: это базовая единица планирования и диспетчеризации ЦП, наименьшая единица выполнения ЦП, наименьшая единица потока выполнения программы и наименьшая единица выполнения программы.

Разница между потоками и процессами:

  • пространство памяти
    • Несколько потоков в процессе используют одно и то же пространство памяти.
    • Несколько процессов имеют независимое пространство памяти
  • Связь между процессами/потоками
    • Простая связь между потоками
    • Межпроцессное взаимодействие — это сложно

Ресурсы потоков

  • Общие ресурсы процесса
    • то же адресное пространство
    • таблица дескрипторов файлов
    • Как обрабатывается каждый сигнал
    • текущий рабочий каталог
    • идентификатор пользователя и идентификатор группы
  • Уникальные ресурсы
    • Стек потоков
    • Каждый поток имеет личную контекстную информацию.
    • Идентификатор темы
    • Регистрируемое значение
    • ошибочное значение
    • Слова маски сигнала и приоритеты планирования

Команды, связанные с потоками

Существует множество команд для просмотра процесса в системе Linux, включая pidstat, top, ps, вы можете просмотреть процесс, вы также можете просмотреть
поток в процессе

команда pidstat

После того, как инструмент sysstat необходимо установить в Ubuntu, может поддерживаться pidstat.

sudo apt install sysstat

Параметры

-t: показать потоки, связанные с указанным процессом

-p: указать идентификатор процесса

Пример

Просмотрите потоки, связанные с процессом 12345.

судо pidstat -t -p 12345

Просмотр потоков, связанных со всеми процессами

судо пидстат -т

Просмотр потоков, связанных с процессом 12345, вывод каждые 1 секунду.

судо pidstat -t -p 12345 1

Просмотр потоков, связанных со всеми процессами, вывод каждые 1 секунду

судо пидстат -т 1

высшее командование

Используйте команду top для просмотра потоков определенного процесса. Вам нужно использовать опцию -H в сочетании с -p, чтобы указать pid.

Параметры

-H: отображать информацию о потоке

-p: указать идентификатор процесса

Пример

Просмотрите потоки, связанные с процессом 12345.

sudo топ -H -p 12345

Просмотр потоков, связанных со всеми процессами

судо топ -H

команда ps

Используйте команду ps в сочетании с опцией -T для просмотра всех потоков процесса.

Параметры

-T: отображать информацию о потоке

-p: указать идентификатор процесса

Пример

Просмотрите потоки, связанные с процессом 12345.

судо пс -Т -р 12345

Просмотр потоков, связанных со всеми процессами

судо пс -Т

Распространенные сценарии параллелизма

1. Многопроцессный режим

В многопроцессном режиме каждый процесс отвечает за разные задачи, не мешая друг другу. Каждый процесс выполняется в отдельном пространстве памяти, не влияя друг на друга.

  • преимущество:
    • Адресное пространство процесса независимо. Если в процессе возникает исключение, оно не влияет на другие процессы.
  • недостаток:
    • Каждому процессу необходимо выделить независимое пространство памяти. Создание процесса является дорогостоящим и требует больше памяти.
    • Межпроцессное сотрудничество и межпроцессное взаимодействие более сложны.
  • Применимая сцена:
    • Несколько задач не очень тесно связаны между собой, поэтому можно использовать многопроцессный режим.
    • Между задачами нет зависимостей, можно использовать многопроцессный режим.

2. Многопоточный режим

В многопоточном режиме в процессе может быть несколько потоков, использующих одно и то же пространство памяти, и потоки могут взаимодействовать напрямую.

  • преимущество:
    • Связь между потоками проста
    • Несколько потоков одного и того же процесса могут совместно использовать ресурсы и улучшать их использование.
  • недостаток:
    • Потоки не имеют независимых адресных пространств процесса. После завершения основного потока другие потоки также завершатся.
    • Переключение потоков и планирование требуют ресурсов. Слишком много потоков будут потреблять системные ресурсы.
    • Синхронизация между потоками сложна, и необходимо учитывать вопросы безопасности потоков.
  • Применимая сцена:
    • Существуют зависимости между задачами и можно использовать многопоточный режим.
    • Обмен данными между задачами происходит относительно часто, и можно использовать многопоточный режим.

Создать тему

1. pthread_create()

pthread_create() используется для создания потока. После успешного создания поток начинает работать.
После успешного вызова pthread_create() он вернет 0, в противном случае он вернет код ошибки.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • поток: указатель на тип pthread_t, используемый для хранения идентификатора потока.
  • attr: атрибут потока, который может иметь значение NULL, указывая, что используется атрибут по умолчанию.
  • start_routine: функция входа потока.
  • arg: Параметры, передаваемые в функцию входа в поток.

возвращаемое значение:

  • 0: Создано успешно.
  • EAGAIN: Недостаточно ресурсов, не удалось создать поток.
  • EINVAL: параметр недействителен.
  • ENOMEM: Недостаточно памяти, не удалось создать поток.

Уведомление:

  • После успешного создания дочернего потока он будет независимо запланирован для выполнения и будет выполняться одновременно с другими потоками.
  • Библиотеку -lpthread необходимо связать при компиляции.

Пример: создать тему

// 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() завершает поток

pthread_exit() используется для выхода из потока. После завершения выполнения потока автоматически вызывается pthread_exit() для выхода.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • retval: значение, возвращаемое при выходе из потока.
  • После выполнения функции потока для выхода будет автоматически вызвана функция pthread_exit().

3. pthread_join() ждет завершения потока

pthread_join() используется для ожидания завершения потока,
После вызова pthread_join() текущий поток будет заблокирован до тех пор, пока поток не завершится.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • поток: идентификатор потока.
  • retval: указатель на возвращаемое значение потока, используемый для хранения значения, возвращаемого при выходе из потока. (вторичный указатель)

возвращаемое значение:

  • 0: Ждите успеха.
  • EINVAL: параметр недействителен.
  • ESRCH: идентификатор потока не существует.
  • EDEADLK: поток находится в состоянии взаимоблокировки.

Пример:

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

Разделение ниток

Нити делятся на соединяемые и разъемные.

  • Можно комбинировать
    • Комбинируемый поток может вернуть свои ресурсы и быть уничтожен другими потоками; его ресурсы памяти (например, стек) не освобождаются до тех пор, пока они не будут освобождены другими потоками.
    • Состояние создания потока по умолчанию является комбинируемым. Функция pthread_join может быть вызвана другими потоками для ожидания завершения дочернего потока и освобождения связанных ресурсов.
  • отделимый
    • Он не может быть перезапущен или уничтожен другими потоками. Ресурсы потока освобождаются системой при его завершении.
// 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

Создать несколько тем

Пример 1. Создайте несколько потоков для выполнения разных задач.

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

Пример 2. Создайте несколько потоков для выполнения одной и той же задачи.

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

Связь между потоками

Другая связь между процессами также применима к связи между потоками.

Основной поток передает параметры дочернему потоку.

При создании дочернего потока с помощью функции pthread_create() четвертый параметр pthread_create() — это параметр, передаваемый функции дочернего потока.

Подпоток передает параметры в основной поток

При выходе из дочернего потока через функцию pthread_exit() вы можете передать параметры основному потоку.

void pth_exit(void *retval);
  • 1

При ожидании окончания подпотока через функцию pthread_join() получите возвращаемые параметры подпотока.

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

Пример:

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

Мьютекс потока

Мьютекс потока

Мьютекс — это механизм синхронизации, используемый для управления доступом к общим ресурсам.

Основным преимуществом потоков является возможность обмениваться информацией через глобальные переменные, но за такое удобное разделение приходится платить:

Необходимо гарантировать, что несколько потоков не изменяют одну и ту же переменную одновременно.

Поток не будет читать переменные, изменяемые другими потоками. Фактически, два потока не могут одновременно получить доступ к критической секции.

Принцип блокировки мьютекса

Принцип блокировки мьютекса заключается в том, что когда поток пытается войти в область мьютекса, если эта область уже занята другими потоками, поток будет заблокирован до тех пор, пока область мьютекса не будет освобождена.

По сути, это переменная типа pthread_mutex_t, которая содержит целочисленное значение, обозначающее состояние области мьютекса.
Когда значение равно 1, это означает, что текущий критический ресурс может конкурировать за доступ, и поток, получивший блокировку мьютекса, может войти в критическую секцию. В это время значение равно 0, и другие потоки могут только ждать.
Когда значение равно 0, это означает, что текущий критический ресурс занят другими потоками и не может войти в критическую секцию и может только ждать.

Характеристики мьютексных блокировок

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
  • Блокировка мьютекса — это переменная типа pthread_mutex_t, которая представляет блокировку мьютекса.
  • Если два потока обращаются к одной и той же переменной pthread_mutex_t, они получают доступ к одной и той же блокировке мьютекса.
  • Соответствующие переменные определены в заголовочном файле pthreadtypes.h, который представляет собой общий текст, содержащий структуру.

Использование мьютексных блокировок

Существует два основных способа инициализации блокировок мьютексов потоков:

статическая инициализация

  • Определите переменную типа pthread_mutex_t и инициализируйте ее значением PTHREAD_MUTEX_INITIALIZER.
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
  • 1

динамическая инициализация

Динамическая инициализация Динамическая инициализация в основном включает в себя две функции: функцию pthread_mutex_init и функцию pthread_mutex_destroy.

функция pthread_mutex_init()

Используется для инициализации блокировки мьютекса и принимает два параметра: адрес блокировки мьютекса и атрибуты блокировки мьютекса.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • мьютекс: указатель на тип pthread_mutex_t, используемый для хранения адреса блокировки мьютекса.
  • attr: атрибут блокировки мьютекса, который может иметь значение NULL, что указывает на то, что используется атрибут по умолчанию.

возвращаемое значение:

  • 0: Инициализация прошла успешно.
  • В случае сбоя возвращается код ошибки.
функция pthread_mutex_destroy()

Используется для разрушения блокировки мьютекса и принимает один параметр: адрес блокировки мьютекса.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • мьютекс: указатель на тип pthread_mutex_t, используемый для хранения адреса блокировки мьютекса.

возвращаемое значение:

  • 0: Уничтожение успешно.
  • В случае сбоя возвращается код ошибки.

Пример:

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

Синхронизация потоков

Синхронизация потоков: относится к упорядоченному доступу посетителей к ресурсам через другие механизмы на основе взаимного исключения (в большинстве случаев).

Условная переменная: механизм, предоставляемый библиотекой потоков специально для синхронизации потоков.

Типичный сценарий приложения для синхронизации потоков — между производителями и потребителями.

Проблемы производителей и потребителей

В этой модели он разделен на поток-производитель и поток-потребитель, и через этот поток моделируется процесс синхронизации нескольких потоков.

В данной модели необходимы следующие компоненты:

  • Склад: используется для хранения продуктов, как правило, как общий ресурс.
  • Резьба производителя: используется для производства продукции
  • Потребительская нить: используется для потребления продуктов.

принцип:

Когда на складе нет продукта, потребительскому потоку необходимо дождаться появления продукта, прежде чем он сможет его потребить.

Когда склад заполнен продуктами, поток-производитель должен дождаться, пока поток-потребитель не израсходует продукты.

Пример реализации моделей производителя и потребителя на основе блокировок мьютексов

Основная нить – потребитель

n подпотоков в качестве производителей

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

переменная условия

Условная переменная — это механизм синхронизации, который позволяет потоку дождаться выполнения определенного условия, прежде чем продолжить работу.

Принцип условной переменной заключается в том, что она содержит блокировку мьютекса и очередь ожидания.

Блокировки мьютексов используются для защиты очередей ожидания и переменных условий.

Вставьте сюда описание изображения

Инициализация переменной условия

Условная переменная имеет тип 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

Инициализация условных переменных делится на статическую инициализацию и динамическую инициализацию.

статическая инициализация

Для статически инициализированных переменных условия необходимо сначала определить переменную типа pthread_cond_t, а затем инициализировать ее значением PTHREAD_COND_INITIALIZER.

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 1

Динамическая инициализация pthread_cond_init()

Чтобы динамически инициализировать условную переменную, вам необходимо сначала определить переменную типа pthread_cond_t, а затем вызвать функцию pthread_cond_init для ее инициализации.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • cond: указатель на тип pthread_cond_t, используемый для хранения адреса условной переменной.
  • attr: атрибут условной переменной, который может иметь значение NULL, чтобы использовать атрибут по умолчанию.

возвращаемое значение:

  • 0: Инициализация прошла успешно.
  • В случае сбоя возвращается код ошибки.

pthread_cond_destroy()

Используется для уничтожения условных переменных и принимает один параметр: адрес условной переменной.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • cond: указатель на тип pthread_cond_t, используемый для хранения адреса условной переменной.

возвращаемое значение:

  • 0: Уничтожение успешно.
  • В случае сбоя возвращается код ошибки.

Использование условных переменных

Использование условных переменных делится на ожидание и уведомление.

Подождите pthread_cond_wait().

Функция ожидания pthread_cond_wait() принимает три параметра: адрес условной переменной, адрес блокировки мьютекса и время ожидания.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • cond: указатель на тип pthread_cond_t, используемый для хранения адреса условной переменной.
  • мьютекс: указатель на тип pthread_mutex_t, используемый для хранения адреса блокировки мьютекса.
  • abstime: тайм-аут, который может иметь значение NULL, что указывает на отсутствие тайм-аута.

возвращаемое значение:

  • 0: Ждите успеха.
  • В случае сбоя возвращается код ошибки.

Уведомить pthread_cond_signal()

функция уведомления
pthread_cond_signal() принимает один параметр: адрес условной переменной.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • cond: указатель на тип pthread_cond_t, используемый для хранения адреса условной переменной.

возвращаемое значение:

  • 0: уведомление успешно.
  • В случае сбоя возвращается код ошибки.

Уведомить всех pthread_cond_broadcast()

Уведомить все функции
pthread_cond_broadcast() принимает один параметр: адрес условной переменной.

Заголовочный файл функции:

#include <pthread.h>

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

Описание параметра:

  • cond: указатель на тип pthread_cond_t, используемый для хранения адреса условной переменной.

возвращаемое значение:

  • 0: уведомление успешно.
  • В случае сбоя возвращается код ошибки.

Пример реализации моделей производителя и потребителя на основе условных переменных

Вставьте сюда описание изображения

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

Почему условные переменные необходимо использовать вместе с блокировками мьютексов?

Защитите общие данные:

Блокировки мьютексов используются для защиты общих данных и обеспечения того, чтобы только один поток мог одновременно получать доступ к данным и изменять их.

Это позволяет избежать гонок и несогласованности данных.

Переменные условия используются для связи между потоками, чтобы уведомить другие потоки о выполнении определенного условия.

Однако работа с условными переменными сама по себе не обеспечивает защиту общих данных и поэтому ее необходимо использовать в сочетании с блокировкой мьютекса.

Избегайте ложных пробуждений:

Одной из характеристик переменных состояния является то, что может произойти ложное пробуждение.

То есть поток пробуждается без явного уведомления. Во избежание неправильных операций, вызванных этой ситуацией,

Поток должен перепроверить, действительно ли условие выполнено после пробуждения.

Использование мьютекса гарантирует, что общие данные не будут изменены другими потоками при проверке условий, что позволяет избежать ошибок, вызванных ложными пробуждениями.

Убедитесь, что уведомление правильное:

Когда поток уведомляет другие потоки через переменные условия, перед отправкой уведомления ему необходимо убедиться, что общие данные обновлены.

Это гарантирует мьютекс, гарантирующий, что все операции обновления данных будут завершены до снятия блокировки.

Аналогично, поток, получающий уведомление, должен удерживать мьютекс перед проверкой условия, чтобы гарантировать стабильность данных при проверке условия.

Реализуйте сложные шаблоны синхронизации:
Сочетание блокировок мьютексов с условными переменными может реализовать более сложные шаблоны синхронизации, такие как проблемы производитель-потребитель, проблемы чтения-записи и т. д. Мьютексы защищают общие данные, а переменные условия используются для координации и связи между потоками.

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