>  기사  >  php教程  >  Linux 다중 스레드 프로그래밍에 대한 자세한 튜토리얼(스레드는 세마포어를 통해 통신 코드를 구현함)

Linux 다중 스레드 프로그래밍에 대한 자세한 튜토리얼(스레드는 세마포어를 통해 통신 코드를 구현함)

黄舟
黄舟원래의
2016-12-13 09:57:131540검색

스레드 분류

스레드는 스케줄러에 따라 사용자 수준 스레드와 코어 수준 스레드로 나눌 수 있습니다.

(1) 사용자 수준 스레드
사용자 수준 스레드는 주로 컨텍스트 전환 문제를 해결합니다. 스케줄링 알고리즘과 스케줄링 프로세스는 모두 사용자가 결정하며 런타임 시 특정 커널 지원이 필요하지 않습니다. . 여기서 운영 체제는 종종 스레드 생성, 예약, 취소 및 기타 기능을 제공하는 사용자 공간 스레드 라이브러리를 제공하는 반면 커널은 여전히 ​​프로세스만 관리합니다. 프로세스의 스레드가 차단 시스템 호출을 호출하면 프로세스의 다른 모든 스레드를 포함하여 해당 프로세스도 차단됩니다. 이러한 종류의 사용자 수준 스레드의 가장 큰 단점은 프로세스에서 여러 스레드를 예약할 때 다중 프로세서를 활용할 수 없다는 것입니다.

(2) 코어 수준 스레드
이러한 종류의 스레드를 사용하면 서로 다른 프로세스의 스레드를 동일한 상대적 우선 순위 예약 방법에 따라 예약할 수 있으므로 다중 프로세서의 동시성 이점을 활용할 수 있습니다.
이제 대부분의 시스템은 사용자 수준 스레드와 코어 수준 스레드가 공존하는 방식을 채택합니다. 사용자 수준 스레드는 "일대일" 또는 "다대일" 모델인 하나 이상의 코어 수준 스레드에 해당할 수 있습니다. 이는 다중 프로세서 시스템의 요구 사항을 충족할 뿐만 아니라 예약 오버헤드도 최소화합니다.

Linux 스레드 구현은 코어 외부에서 수행되며, 코어는 프로세스 생성을 위한 인터페이스 do_fork()를 제공합니다. 커널은 두 개의 시스템 호출인 clone()과 fork()를 제공하는데, 이는 궁극적으로 서로 다른 매개변수를 사용하여 do_fork() 커널 API를 호출합니다. 물론 스레드를 구현하려면 다중 프로세스(실제로는 경량 프로세스) 공유 데이터 세그먼트에 대한 핵심 지원이 없으면 불가능합니다. 따라서 do_fork()는 CLONE_VM(공유 메모리 공간), CLONE_FS(공유 파일)을 포함한 많은 매개변수를 제공합니다. 시스템 정보), CLONE_FILES(공유 파일 설명자 테이블), CLONE_SIGHAND(공유 신호 핸들 테이블) 및 CLONE_PID(공유 프로세스 ID, 핵심 프로세스, 즉 프로세스 0에만 유효). 포크 시스템 호출을 사용할 때 커널은 공유 속성을 사용하지 않고 do_fork()를 호출합니다. 프로세스는 독립적인 실행 환경을 가지며 다음을 사용합니다. pthread_create()를 사용하여 스레드를 생성하면 이러한 모든 속성이 최종적으로 __clone()을 호출하도록 설정되고 이러한 모든 매개 변수는 코어의 do_fork()에 전달됩니다. 이렇게 생성된 "프로세스"는 공유 실행 환경을 가지며 오직 스택은 독립적이며 __clone()에 의해 전달됩니다.

Linux 스레드는 독립적인 프로세스 테이블 항목을 포함하여 코어 내에 경량 프로세스 형태로 존재하며 모든 생성, 동기화, 삭제 및 기타 작업은 코어 외부의 pthread 라이브러리에서 수행됩니다. pthread 라이브러리는 관리 스레드(__pthread_manager(), 각 프로세스는 독립적이고 고유함)를 사용하여 스레드 생성 및 종료를 관리하고, 스레드 ID를 스레드에 할당하고, 스레드 관련 신호(예: 취소)를 보냅니다. pthread_create()) 호출자는 요청 정보를 파이프라인을 통해 관리 스레드에 전달합니다.

주요 기능 설명

1. 스레드 생성 및 종료

pthread_create 스레드 생성 함수
int pthread_create (pthread_t * thread_id,__const pthread_attr_t * __attr,void *(*__start_routine) (void *),void *__restrict __arg);

스레드 생성 함수의 첫 번째 매개변수는 스레드 식별자에 대한 포인터이고, 두 번째 매개변수는 스레드 속성을 설정하는 데 사용되며, 세 번째 매개변수는 스레드 실행 함수의 시작 주소이며, 마지막 매개변수는 함수를 실행하기 위한 매개변수입니다. 여기, 함수 스레드가 있습니다. 매개변수가 필요하지 않으므로 마지막 매개변수는 널 포인터로 설정됩니다. 또한 두 번째 매개변수를 널 포인터로 설정하여 기본 속성을 가진 스레드를 생성합니다. 스레드가 성공적으로 생성되면 함수는 0이 아닌 경우 0을 반환합니다. 스레드 생성에 실패했으며 일반적인 오류 반환 코드는 EAGAIN임을 의미합니다. 그리고 EINVAL. 전자는 시스템이 새로운 스레드 생성을 제한한다는 것을 의미합니다. 예를 들어 스레드 수가 너무 많다는 것은 두 번째 매개변수가 나타내는 스레드 속성 값이 불법임을 의미합니다. 스레드가 성공적으로 생성된 후 새로 생성된 스레드는 매개변수 3과 매개변수 4에 의해 결정된 함수를 실행하고 원래 스레드는 계속해서 다음 코드 줄을 실행합니다.

스레드의 끝을 기다리는 pthread_join 함수입니다.
함수 프로토타입은 다음과 같습니다: int pthread_join (pthread_t __th, void **__thread_return)
첫 번째 매개변수는 대기 중인 스레드의 식별자이고, 두 번째 매개변수는 대기 중인 스레드의 반환 값을 저장하는 데 사용할 수 있는 사용자 정의 포인터입니다. 이 함수는 스레드 차단 함수입니다. 이를 호출하는 함수는 대기 스레드가 끝날 때까지 대기합니다. 함수가 반환되면 대기 스레드의 리소스가 복구됩니다. 스레드는 하나의 스레드에 의해서만 종료될 수 있으며 결합 가능한 상태(분리되지 않음)에 있어야 합니다.

pthread_exit 함수
스레드를 종료하는 방법에는 두 가지가 있습니다. 하나는 스레드에서 실행 중인 함수가 종료되면 이를 호출한 스레드도 종료되는 것입니다.
다른 방법은 pthread_exit 함수를 사용하는 것입니다. 달성하기 위해. 함수 프로토타입은 void pthread_exit(void *__retval)입니다. 유일한 매개 변수는 pthread_join 동안 함수의 반환 코드입니다. 두 번째 매개변수 thread_return NULL이 아니며 이 값은 thread_return에 전달됩니다. 마지막으로 주의할 점은 여러 스레드가 하나의 스레드를 기다릴 수 없다는 것입니다. 그렇지 않으면 신호를 수신한 첫 번째 스레드가 성공적으로 반환되고 나머지는 pthread_join을 호출합니다. 스레드가 오류 코드 ESRCH를 반환합니다.

2. 스레드 속성

pthread_create 함수의 두 번째 매개변수 스레드의 속성입니다. 이 값을 NULL로 설정하십시오. 즉, 스레드의 많은 속성을 변경할 수 있습니다. 이러한 속성에는 주로 바인딩 속성, 분리 속성, 스택 주소, 스택 크기 및 우선 순위가 포함됩니다. 시스템 기본 속성은 바인딩되지 않고 분리되지 않으며 기본값은 1M입니다. 스택이며 상위 프로세스와 동일한 우선순위 수준입니다. 다음은 먼저 바인딩된 속성과 분리된 속성의 기본 개념을 설명합니다.

바인딩 속성: Linux는 "일대일" 스레드 메커니즘을 채택합니다. 즉, 하나의 사용자 스레드가 하나의 커널 스레드에 해당합니다. 바인딩 속성은 CPU 시간 분할 예약이 커널 스레드에 대한 것이기 때문에 사용자 스레드가 커널 스레드에 고정적으로 할당됨을 의미합니다. (즉, 경량 프로세스) 바인딩 속성이 있는 스레드는 필요할 때 항상 그에 상응하는 커널 스레드가 있음을 보장할 수 있습니다. 반면, 비바인딩 속성은 사용자 스레드와 커널 스레드 간의 관계가 항상 고정되는 것이 아니라 시스템에 의해 제어되고 할당된다는 것을 의미합니다.

분리 속성: 분리 속성은 스레드가 스스로 종료되는 방식을 결정하는 데 사용됩니다. 비분리의 경우 스레드가 종료될 때 스레드가 점유하고 있는 시스템 리소스가 해제되지 않습니다. 즉, 실제 종료가 없습니다. pthread_join() 함수가 반환될 때만 생성된 스레드가 차지하고 있는 시스템 리소스를 해제할 수 있습니다. 분리된 속성의 경우, 스레드가 종료되면 해당 속성이 차지하는 시스템 리소스가 즉시 해제됩니다.
여기서 주목해야 할 점은 스레드의 분리 속성을 설정하고 이 스레드가 매우 빠르게 실행되면 pthread_create에 있을 가능성이 높다는 것입니다. 함수가 반환되기 전에 종료됩니다. 종료된 후에는 스레드 번호와 시스템 리소스가 다른 스레드로 넘겨질 수 있습니다. 이때 pthread_create를 호출하는 스레드는 잘못된 스레드 번호를 얻게 됩니다.

바인딩 속성 설정:

int pthread_attr_init(pthread_attr_t *attr)
int pthread_attr_setscope(pthread_attr_t *attr, int 범위)
int pthread_attr_getscope(pthread_attr_t *tattr, int *scope)
scope: PTHREAD_SCOPE_SYSTEM: 바인딩, 이 스레드는 시스템의 모든 스레드와 경쟁합니다. PTHREAD_SCOPE_PROCESS: 바인딩 해제됨, 이 스레드는 프로세스의 다른 스레드와 경쟁합니다.

분리 속성 설정:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
int pthread_attr_getdetachstate(const pthread_attr_t *tattr,int *detachstate)
detachstate PTHREAD_CREATE_DETACHED: PTHREAD 분리 _CREATE_JOINABLE: 분리되지 않음

일정 정책 설정:

int pthread_attr_setschedpolicy(pthread_attr_t * tattr, intpolicy)
int pthread_attr_getschedpolicy(pthread_attr_t * tattr, int *policy)
정책 SCHED_FIFO: 선입선출 SCHED_RR: 루프 SCHED_OTHER: 구현 정의 메서드

우선순위 설정:

int pthread_attr_setschedparam (pthread_attr_t *attr, struct sched_param *param)
int pthread_attr_getschedparam(pthread_attr_t *attr, struct sched_param *param)

3. 스레드 접근 제어

1) 뮤텍스 잠금(mutex)
은 잠금 메커니즘을 통해 스레드 간 동기화를 달성합니다. 한 번에 하나의 스레드만 코드의 중요한 섹션 하나를 실행할 수 있습니다.

1 int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
2 int pthread_mutex_lock(pthread_mutex_t *mutex);
3 int pthread_mutex_unlock(pthread_mutex_t *mutex);
4 int pthread_mutex_destroy(pthread_mutex_t *mutex);

(1) 먼저 잠금 init()를 초기화하거나 pthread_mutex_t를 정적으로 할당합니다. mutex=PTHREAD_MUTEX_INITIALIER
(2) 잠금, 잠금, trylock, 잠금을 기다리는 잠금 블록, trylock은 즉시 EBUSY를 반환
(3) 잠금 해제, 잠금 해제는 잠금 상태에 있어야 하며 잠금 스레드에 의해 잠금 해제됩니다
(4) 잠금을 해제하고 삭제합니다. (이때 잠금을 해제해야 하며 그렇지 않으면 EBUSY가 반환됩니다.)

뮤텍스는 재귀형과 비재귀형 두 가지로 나누어집니다. 이것이 POSIX의 이름입니다. 게다가 이름은 Reentrant 입니다. 비재진입. 스레드 간 동기화 도구로 사용될 때 이 두 뮤텍스 사이에는 차이가 없습니다. 유일한 차이점은 동일한 스레드가 반복적으로 재귀적으로 뮤텍스를 수행할 수 있다는 것입니다. 뮤텍스는 잠겨 있지만 비재귀적 뮤텍스는 반복적으로 잠글 수 없습니다. 잠그다.
비재귀적 뮤텍스가 선호됩니다. 성능이 아니라 디자인 의도를 반영하기 위해서입니다. 비재귀적 및 재귀적 카운터를 하나 적게 사용하고 전자가 조금 더 빠르기 때문에 둘 사이의 성능 차이는 실제로 크지 않습니다. 동일한 스레드에서 비재귀적 뮤텍스를 여러 번 반복합니다. 잠금은 즉시 교착 상태를 발생시킵니다. 이것이 코드의 잠금 요구 사항을 생각하고 (코딩 단계에서) 문제를 조기에 발견하는 데 도움이 될 수 있다는 것이 장점이라고 생각합니다. 의심할 바 없이 재귀적 뮤텍스 스레드 잠금 자체에 대해 걱정할 필요가 없기 때문에 사용하기가 더 편리합니다. 이것이 Java와 Windows가 기본적으로 재귀적 뮤텍스를 제공하는 이유인 것 같습니다. (자바 언어와 함께 제공되는 내장 잠금은 재진입 가능하며 동시에 가능합니다. 라이브러리는 ReentrantLock을 제공하며 Windows의 CRITICAL_SECTION도 재진입이 가능합니다. 그들 중 누구도 가벼운 비재귀적 기능을 제공하지 않는 것 같습니다. 뮤텍스. )

2) 조건변수(cond)
동기화를 위해 스레드간 공유되는 전역변수를 사용하는 메커니즘.

1 int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
2 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
3 int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const timespec *abstime);
4 int pthread_cond_destroy(pthread_cond_t *cond)
5 int pthread_cond_signal(pthread_cond_t *cond);
6 int pthread_cond_broadcast(pthread_cond_t *cond); //모든 스레드 차단 해제


(1) 초기화() 또는 pthread_cond_t cond=PTHREAD_COND_INITIALIER; 속성을 NULL로 설정
(2) 조건이 설정될 때까지 기다립니다. pthread_cond_wait,pthread_cond_timedwait.
wait()는 잠금을 해제하고 조건 변수가 true가 될 때까지 기다리는 것을 차단합니다
timedwait()는 대기 시간을 설정하지만 여전히 신호가 없으며 ETIMEOUT을 반환합니다(잠금은 대기 중인 스레드가 하나만 있음을 보장합니다) )
(3) 조건 변수 활성화:pthread_cond_signal,pthread_cond_broadcast (모든 대기 스레드 활성화)
(4) 조건 변수 지우기:destroy; 대기 중인 스레드가 없습니다. 그렇지 않으면 EBUSY

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

가 반환됩니다. 이 두 함수는 뮤텍스 잠금 영역 내에서 사용해야 합니다.

pthread_cond_signal() 호출 조건부로 차단된 스레드를 해제할 때 조건 변수에 따라 차단된 스레드가 없으면 pthread_cond_signal()을 호출해도 아무런 효과가 없습니다. Windows의 경우 전화할 때 SetEvent가 자동 재설정 이벤트 조건을 트리거할 때 조건에 의해 차단된 스레드가 없으면 이 함수는 계속 작동하고 조건 변수는 트리거된 상태에 있습니다.

Linux에서의 생산자-소비자 문제(뮤텍스 및 조건 변수 사용):

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "pthread.h"
#define BUFFER_SIZE 16
struct prodcons  
{  
int buffer[BUFFER_SIZE];  
pthread_mutex_t lock;  //mutex ensuring exclusive access to buffer
int readpos,writepos;  //position for reading and writing
pthread_cond_t notempty;  //signal when buffer is not empty
pthread_cond_t notfull;  //signal when buffer is not full
};  
//initialize a buffer
void init(struct prodcons* b)  
{  
pthread_mutex_init(&b->lock,NULL);  
pthread_cond_init(&b->notempty,NULL);  
pthread_cond_init(&b->notfull,NULL);  
b->readpos = 0;  
b->writepos = 0;  
}  
//store an integer in the buffer
void put(struct prodcons* b, int data)  
{  
pthread_mutex_lock(&b->lock);  
//wait until buffer is not full
while((b->writepos+1)%BUFFER_SIZE == b->readpos)  
{  
printf("wait for not full\n");  
pthread_cond_wait(&b->notfull,&b->lock);  
}
b->buffer[b->writepos] = data;  
b->writepos++;
b->writepos %= BUFFER_SIZE;
pthread_cond_signal(&b->notempty); //signal buffer is not empty
pthread_mutex_unlock(&b->lock);  
}
//read and remove an integer from the buffer
int get(struct prodcons* b)  
{  
int data;  
pthread_mutex_lock(&b->lock);  
//wait until buffer is not empty
while(b->writepos == b->readpos)  
{  
printf("wait for not empty\n");  
pthread_cond_wait(&b->notempty,&b->lock);  
}
data=b->buffer[b->readpos];  
b->readpos++;
b->readpos %= BUFFER_SIZE;
pthread_cond_signal(&b->notfull);  //signal buffer is not full
pthread_mutex_unlock(&b->lock);  
return data;
}
#define OVER -1
struct prodcons buffer;  
void * producer(void * data)  
{  
int n;  
for(n=0; n<50; ++n)  
{
printf("put-->%d\n",n);  
put(&buffer,n);  
}  
put(&buffer,OVER);  
printf("producer stopped\n");  
return NULL;  
}  
void * consumer(void * data)  
{  
int n;  
while(1)  
{  
int d = get(&buffer);  
if(d == OVER) break;  
printf("get-->%d\n",d);  
}
printf("consumer stopped\n");  
return NULL;  
}  
int main()  
{  
pthread_t tha,thb;  
void * retval;  
init(&buffer);  
pthread_creare(&tha,NULL,producer,0);  
pthread_creare(&thb,NULL,consumer,0);  
pthread_join(tha,&retval);  
pthread_join(thb,&retval);  
return 0;  
}

3) 세마포
프로세스와 마찬가지로 스레드도 세마포를 사용하여 통신을 구현할 수 있지만 가볍습니다.

세마포어 함수의 이름은 모두 "sem_"으로 시작합니다. 스레드에서 사용하는 네 가지 기본 세마포어 함수가 있습니다.

#include <semaphore.h>
int sem_init(sem_t *sem , int pshared, unsigned int value);

sem으로 지정된 세마포어를 초기화하고 공유 옵션을 설정한 다음(리눅스는 0만 지원하므로 현재 프로세스의 로컬 세마포어임을 의미), 초기값 VALUE.


두 개의 원자 연산 함수: 두 함수 모두 sem_init 호출에 의해 초기화된 세마포 객체에 대한 포인터를 매개변수로 사용합니다.

int sem_wait(sem_t *sem); //给信号量减1,对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。
int sem_post(sem_t *sem); //给信号量的值加1
int sem_destroy(sem_t *sem);

이 함수의 기능은 세마포어를 사용한 후 정리하는 것입니다. 가지고 있는 자원을 모두 돌려주세요.

세마포어를 사용하여 생산자와 소비자 구현:

여기서는 4개의 세마포가 사용되며, 그 중 두 개의 세마포(점유 및 비어 있음)는 각각 생산자와 소비자 스레드 간의 동기화 문제를 해결하는 데 사용됩니다. 즉, pmut는 여러 생산자 간의 상호 배제에 사용되고, cmut는 여러 소비자 간의 상호 배제에 사용됩니다. 그 중,empty는 N(bounded buffer의 공간 요소 수)으로 초기화되고, occuped는 0으로, pmut, cmut는 1로 초기화됩니다.

참조 코드:

#define BSIZE 64
typedef struct 
{
char buf[BSIZE];
sem_t occupied;
sem_t empty;
int nextin;
int nextout;
sem_t pmut;
sem_t cmut;
}buffer_t;
buffer_t buffer;
void init(buffer_t * b)
{
sem_init(&b->occupied, 0, 0);
sem_init(&b->empty,0, BSIZE);
sem_init(&b->pmut, 0, 1);
sem_init(&b->cmut, 0, 1);
b->nextin = b->nextout = 0;
}
void producer(buffer_t *b, char item) 
{
sem_wait(&b->empty);
sem_wait(&b->pmut);
b->buf[b->nextin] = item;
b->nextin++;
b->nextin %= BSIZE;
sem_post(&b->pmut);
sem_post(&b->occupied);
}
char consumer(buffer_t *b) 
{
char item;
sem_wait(&b->occupied);
sem_wait(&b->cmut);
item = b->buf[b->nextout];
b->nextout++;
b->nextout %= BSIZE;
sem_post(&b->cmut);
sem_post(&b->empty);
return item;
}


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.