기술나눔

Linux 다중 프로세스 및 다중 스레딩 (8) 다중 스레딩

2024-07-12

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

멀티스레딩

스레드 정의

스레드는 프로세스의 실행 단위입니다.

현재 프로세스에서 프로그램 실행을 담당하며,

프로세스에 하나 이상의 스레드가 있습니다

하나의 프로세스에 여러 개의 스레드가 있을 수 있습니다.

다중 스레드는 동일한 프로세스의 모든 리소스를 공유하며 각 스레드는 운영 체제의 통합 스케줄링에 참여합니다.

간단히 말해서 프로세스 = 메모리 리소스 + 메인 스레드 + 하위 스레드 +…로 이해될 수 있습니다.

스레드와 프로세스

밀접하게 관련된 작업의 경우 동시성 중에는 멀티스레딩이 선호됩니다. 밀접하게 관련되지 않고 상대적으로 독립적인 작업의 경우 다중 프로세스를 선택하는 것이 좋습니다.

  • 프로세스(Process) : 운영체제에 의한 자원 할당의 기본 단위로, 자원 할당의 최소 단위, 프로그램의 실행 및 스케줄링 단위, 프로그램의 실행 인스턴스이다.
  • 스레드: CPU 스케줄링 및 디스패치의 기본 단위, CPU 실행의 최소 단위, 프로그램 실행 흐름의 최소 단위, 프로그램 실행의 최소 단위입니다.

스레드와 프로세스의 차이점:

  • 기억 공간
    • 프로세스의 여러 스레드가 동일한 메모리 공간을 공유합니다.
    • 여러 프로세스가 독립적인 메모리 공간을 가짐
  • 프로세스/스레드 간 통신
    • 스레드 간 간단한 통신
    • 프로세스 간 통신이 복잡함

스레드 리소스

  • 공유 프로세스 리소스
    • 동일한 주소 공간
    • 파일 설명자 테이블
    • 각 신호가 처리되는 방식
    • 현재 작업 디렉토리
    • 사용자 ID 및 그룹 ID
  • 고유한 리소스
    • 스레드 스택
    • 각 스레드에는 비공개 컨텍스트 정보가 있습니다.
    • 스레드 ID
    • 레지스터 값
    • 오류 값
    • 신호 마스크 단어 및 스케줄링 우선순위

스레드 관련 명령

pidstat, top, ps를 포함하여 Linux 시스템의 프로세스를 볼 수 있는 많은 명령이 있습니다. 프로세스를 볼 수 있고
프로세스 중인 스레드

pidstat 명령

sysstat 도구를 ubuntu에 설치해야 하면 pidstat가 지원될 수 있습니다.

sudo apt install sysstat

옵션

-t: 지정된 프로세스와 연관된 스레드를 표시합니다.

-p: 프로세스 pid 지정

프로세스 12345와 관련된 스레드 보기

sudo pidstat -t -p 12345

모든 프로세스와 관련된 스레드 보기

sudo pidstat -t

프로세스 12345와 관련된 스레드 보기, 1초마다 출력

sudo pidstat -t -p 12345 1

모든 프로세스와 관련된 스레드 보기, 1초마다 출력

sudo pidstat -t 1

최고 사령부

특정 프로세스의 스레드를 보려면 top 명령을 사용하십시오. pid를 지정하려면 -p와 함께 -H 옵션을 사용해야 합니다.

옵션

-H: 스레드 정보 표시

-p: 프로세스 pid 지정

프로세스 12345와 관련된 스레드 보기

sudo 탑 -H -p 12345

모든 프로세스와 관련된 스레드 보기

sudo 탑 -H

ps 명령

프로세스 아래의 모든 스레드를 보려면 -T 옵션과 함께 ps 명령을 사용하십시오.

옵션

-T: 스레드 정보 표시

-p: 프로세스 pid 지정

프로세스 12345와 관련된 스레드 보기

수도 ps -T -p 12345

모든 프로세스와 관련된 스레드 보기

수도 ps -T

일반적인 동시성 시나리오

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

매개변수 설명:

  • thread: 스레드 ID를 저장하는 데 사용되는 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

매개변수 설명:

  • 스레드: 스레드 ID입니다.
  • retval: 스레드가 종료될 때 반환된 값을 저장하는 데 사용되는 스레드 반환 값에 대한 포인터입니다. (보조 포인터)

반환 값:

  • 0: 성공을 기다립니다.
  • EINVAL: 매개변수가 유효하지 않습니다.
  • ESRCH: 스레드 ID가 존재하지 않습니다.
  • 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

스레드 뮤텍스

스레드 뮤텍스

Mutex는 공유 리소스에 대한 액세스를 제어하는 ​​데 사용되는 동기화 메커니즘입니다.

스레드의 주요 장점은 전역 변수를 통해 정보를 공유할 수 있다는 점이지만, 이렇게 편리한 공유에는 대가가 따릅니다.

여러 스레드가 동시에 동일한 변수를 수정하지 않도록 해야 합니다.

특정 스레드는 다른 스레드에 의해 수정되는 변수를 읽지 않습니다. 실제로 두 스레드는 동시에 임계 영역에 액세스할 수 없습니다.

뮤텍스 잠금의 원리

뮤텍스 잠금의 원리는 스레드가 뮤텍스 영역에 들어가려고 할 때 다른 스레드가 이미 뮤텍스 영역을 점유하고 있는 경우 해당 스레드는 뮤텍스 영역이 해제될 때까지 차단된다는 것입니다.

이는 본질적으로 뮤텍스 영역의 상태를 나타내는 정수 값을 포함하는 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

매개변수 설명:

  • mutex: pthread_mutex_t 유형에 대한 포인터로, 뮤텍스 잠금 주소를 저장하는 데 사용됩니다.
  • attr: 기본 속성이 사용됨을 나타내는 NULL일 수 있는 뮤텍스 잠금의 속성입니다.

반환 값:

  • 0: 초기화에 성공했습니다.
  • 실패하면 오류 코드가 반환됩니다.
pthread_mutex_destroy() 함수

뮤텍스 잠금을 파괴하는 데 사용되며 뮤텍스 잠금 주소라는 하나의 매개변수를 허용합니다.

함수 헤더 파일:

#include <pthread.h>

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

매개변수 설명:

  • mutex: 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 유형에 대한 포인터입니다.
  • mutex: 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