스레드 동기화---귀속 자물쇠

개요
가장 흔히 볼 수 있는 프로세스/루틴의 동기화 방법은 상호 배율 자물쇠(또는 상호 배율 Mutex), 읽기와 쓰기 자물쇠(rdlock), 조건 변수(cond), 신호량(Semophore) 등이 있다.Windows 시스템에서도 크리티컬 섹션(Critical Section)과 이벤트 객체(Event)를 동기화하는 데 자주 사용됩니다.
 
간단하게 말하면 상호 배척 자물쇠는 임계 구역을 보호하는데 이 임계 구역에서 한 번에 최대 한 라인만 들어갈 수 있다.만약 여러 프로세스가 같은 임계 구역에서 활동한다면, 경쟁 조건 (racecondition) 으로 인해 오류가 발생할 수 있습니다.
 
읽기와 쓰기 자물쇠는 넓은 의미의 논리적으로도 일종의 공유판의 상호 배척 자물쇠라고 볼 수 있다.만약에 임계 구역의 대부분이 읽기 조작이고 소량의 쓰기 조작만 있다면 읽기와 쓰기 자물쇠는 어느 정도에 라인의 상호 배척이 발생하는 대가를 낮출 수 있다.
 
조건 변수는 어떤 조건이 발생하기를 경쟁이 없는 방식으로 기다릴 수 있도록 허용한다.이 조건이 발생하지 않았을 때, 라인은 줄곧 휴면 상태에 있을 것이다.다른 스레드 알림 조건이 이미 발생했을 때, 스레드가 깨어나 계속 아래로 실행됩니다.조건 변수는 비교적 밑바닥의 동기화 원어로 직접 사용하는 경우가 많지 않고 종종 고위층 간의 루틴 동기화를 실현하는 데 쓰인다.조건 변수를 사용하는 대표적인 예는 스레드 풀(Thread Pool)이다.
 
운영체제의 프로세스 동기화 원리를 배울 때 가장 많이 말하는 것은 신호량이다.신호량을 정성껏 설계한 PV 조작을 통해 매우 복잡한 프로세스 동기화 상황을 실현할 수 있다(예를 들어 고전적인 철학가의 식사 문제와 이발소 문제).현실적인 프로그래밍에서는 신호를 사용하는 사람이 극히 적다.신호량으로 해결할 수 있는 문제는 항상 다른 더욱 명확하고 간결한 디자인 수단으로 신호량을 대체할 수 있을 것 같다. 
 
이 시리즈의 목적은 이러한 동기화 방법을 어떻게 사용해야 하는지를 설명하기 위한 것이 아니다.더 많은 것은 사람들이 소홀히 하기 쉬운 자물쇠에 대한 개념과 비교적 고전적인 사용과 디자인 방법을 설명하는 것이다.문장은 귀속 자물쇠와 비귀속 자물쇠(recursive mutex와non-recursive mutex), 지역 자물쇠(Scoped Lock), 전략 자물쇠(Strategized Locking), 읽기 자물쇠와 조건 변수, 이중 검출 자물쇠(DCL), 자물쇠와 무관한 데이터 구조(Locking free), 자전거 자물쇠 등 내용을 다루고 벽돌을 던져 옥을 끌어올리기를 희망한다.
그러면 우리는 먼저 귀속 자물쇠와 비귀속 자물쇠에서 떠나자.)
 
1  
1.1  

모든 스레드 동기화 방법 중, 아마도 상호 배척 자물쇠 (mutex) 의 등장률은 다른 방법보다 훨씬 높을 것이다.상호 배척 자물쇠의 이해와 기본적인 사용 방법은 모두 매우 쉬우니 여기는 더 이상 소개하지 않겠습니다.
Mutex는 귀속 자물쇠(recursive mutex)와 비귀속 자물쇠(non-recursive mutex)로 나눌 수 있다.귀속 가능한 자물쇠는 다시 들어갈 수 있는 자물쇠(reentrantmutex)라고도 할 수 있으며, 귀속 가능한 자물쇠가 아니면 다시 들어갈 수 없는 자물쇠(non-reentrantmutex)라고도 부른다.
양자의 유일한 차이점은 같은 라인에서 여러 번 같은 귀속 자물쇠를 얻을 수 있고 사쇄가 생기지 않는다는 것이다.한 라인이 같은 비귀속 자물쇠를 여러 번 가져오면 사물쇠가 생성됩니다.
Windows의 Mutex 및 Critical Section은 반복적으로 제공됩니다.Linux의 pthread_mutex_t 자물쇠는 기본적으로 비귀속이다.표시 가능한 설정 PTHREAD_MUTEX_RECURSIVE 속성, pthread_mutex_t는 귀속 자물쇠로 설정됩니다.
대부분의 배척량을 어떻게 사용하는지 소개하는 문장과 책에서 이 두 개념은 종종 무시되거나 얼렁뚱땅 묘사되어 많은 사람들이 이 개념을 전혀 알지 못하게 한다.그러나 이 두 종류의 자물쇠를 잘못 사용하면 프로그램의 자물쇠가 사라질 수 있다.아래 절차를 보십시오.
MutexLock mutex; void foo() { mutex.lock(); // do something mutex.unlock(); } void bar() { mutex.lock(); // do something foo(); mutex.unlock(); } 

foo 함수와bar 함수는 같은 자물쇠를 가져오고bar 함수는foo 함수를 호출합니다.만약 MutexLock 자물쇠가 비귀속 자물쇠라면, 이 프로그램은 즉시 자물쇠를 닫을 것입니다.따라서 프로그램에 자물쇠를 채울 때 각별히 조심해야 한다. 그렇지 않으면 이런 호출 관계로 인해 자물쇠가 사라지기 쉽다.
요행 심리는 존재하지 마라. 이런 상황은 매우 드물다고 생각한다.코드가 어느 정도 복잡하고 여러 사람에게 유지보수되고 호출 관계가 복잡하게 뒤섞일 때 프로그램에서 이런 오류를 범하기 쉽다.다행히도 이런 원인으로 인한 자물쇠는 쉽게 제거된다.
그러나 이것은 비귀속 자물쇠 대신 귀속 자물쇠를 사용해야 한다는 것을 의미하지는 않는다.귀속 자물쇠는 사용하기에는 비록 간단하지만, 왕왕 일부 코드 문제를 숨길 수 있다.예를 들어 호출 함수와 피호출 함수는 자신이 자물쇠를 얻은 줄 알고 같은 대상을 수정하는데 이때 문제가 생기기 쉽다.따라서 비귀속 자물쇠를 사용할 수 있는 상황에서 가능한 한 비귀속 자물쇠를 사용해야 한다. 왜냐하면 사쇄는 상대적으로 디버깅을 통해 발견하기 쉽기 때문이다.프로그램 설계에 문제가 있으면 노출이 빠를수록 좋다.
 
1.2  

상술한 상황으로 인한 자물쇠를 피하기 위해 AUPE v2는 제12장에서 디자인 방법을 제시했다.즉, 만약에 하나의 함수가 이미 잠긴 상태에서 사용할 수도 있고, 잠기지 않은 상황에서 사용할 수도 있다면, 왕왕 이 함수를 두 가지 버전으로 분해한다. 잠금 버전과 잠금 버전 (nolock 접두사 추가) 이다.
예를 들어foo() 함수를 두 함수로 분해합니다.
//   void foo_nolock() { // do something } //   void foo() { mutex.lock(); foo_nolock(); mutex.unlock(); } 

귀속 자물쇠의 실례, 같은 라인에서의 귀속 자물쇠
//   
#include    <stdio.h>  
#include    <stdlib.h>  
#include    <pthread.h>  
pthread_mutex_t g_mutex;  
void test_fun(void);  
static void thread_init(void)  
{  
    //   
    pthread_mutexattr_t attr;  
    pthread_mutexattr_init(&attr);  
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE_NP);//   
      
    //   
    pthread_mutex_init(&g_mutex, &attr);  
      
    //   
    pthread_mutexattr_destroy(&attr);  
}  
//   
void* thr_fun(void* arg)  
{  
    int ret;  
    ret=pthread_mutex_lock(&g_mutex);  
    if( ret!=0 )  
    {  
        perror("thread  pthread_mutex_lock");  
        exit(1);  
    }  
    printf("this is a thread !/n");  
    test_fun();  
    ret=pthread_mutex_unlock(&g_mutex);  
    if( ret!=0 )  
    {  
        perror("thread  pthread_mutex_unlock");  
        exit(1);  
    }  
    return NULL;  
}  
//   
void test_fun(void)  
{  
    int ret;  
    ret=pthread_mutex_lock(&g_mutex);  
    if( ret!=0 )  
    {  
        perror("test pthread_mutex_lock");  
        exit(1);  
    }  
    printf("this is a test!/n");  
    ret=pthread_mutex_unlock(&g_mutex);  
    if( ret!=0 )  
    {  
        perror("test pthread_mutex_unlock");  
        exit(1);  
    }  
}  
  
int main(int argc, char *argv[])  
{  
    int ret;  
    thread_init();  
      
    pthread_t tid;  
    ret=pthread_create(&tid, NULL, thr_fun, NULL);  
    if( ret!=0 )  
    {  
        perror("thread  create");  
        exit(1);  
    }  
    pthread_join(tid, NULL);  
    return 0;  
}  

실행 결과:
this is a thread ! this is a test!
자세한 설명:
유형 배척량 속성은 배척량의 특성을 제어한다.POSIX는 네 가지 유형을 정의합니다.enum    {      PTHREAD_MUTEX_TIMED_NP,       PTHREAD_MUTEX_RECURSIVE_NP,       PTHREAD_MUTEX_ERRORCHECK_NP,      PTHREAD_MUTEX_ADAPTIVE_NP    }; 여기서 PTHREAD_MUTEX_TIMED_NP 유형은 표준 (기본값) 의 상호 배율 유형으로 특별한 오류 검사나 잠금 해제 검사를 하지 않습니다.PTHREAD_MUTEX_RECURSIVE_NP 상호 배척량 유형은 같은 라인이 상호 배척량이 잠기기 전에 이 상호 배척량을 여러 번 잠글 수 있도록 허용한다.같은 귀속 상호 배척량 유지보수 자물쇠의 계수는 자물쇠를 해제하는 횟수와 자물쇠를 추가하는 횟수가 다른 상황에서 자물쇠를 방출하지 않는다.즉, 같은 상호 배척량에 몇 번의 자물쇠를 넣으면 몇 번의 자물쇠를 풀어야 한다.관련된 함수 1.상호 배율 속성의 초기화와 회수 #include        int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);        int pthread_mutexattr_init(pthread_mutexattr_t *attr); 반환 값: 0을 성공적으로 반환하지 않으면 오류 번호를 반환합니다.2. 상호 배율 속성 가져오기/설정 #include        int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,               int *restrict type);        int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); 반환 값: 0을 성공적으로 반환하지 않으면 오류 번호를 반환합니다.테스트 프로그램:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

pthread_mutex_t lock;
int g_val0, g_val1;

int func(void)
{
    int ret, val;
    ret = pthread_mutex_lock(&lock);
    if (ret)
        printf("func:lock:%s
", strerror(ret)); val = g_val1+8; #if 1 ret = pthread_mutex_unlock(&lock); if (ret) printf("func:unlock%s
", strerror(ret)); #endif return val; } void * test0(void * arg) { int ret; ret = pthread_mutex_lock(&lock); if (ret) printf("lock:%s
", strerror(ret)); sleep(5); g_val0 = func(); printf("res=%d
", g_val0); ret = pthread_mutex_unlock(&lock); if (ret) printf("unlock%s
", strerror(ret)); return NULL; } void * test1(void * arg) { sleep(1); #if 1 int ret = pthread_mutex_lock(&lock); if (ret) printf("1:%s
", strerror(ret)); printf("g_val0=%d
", g_val0); ret = pthread_mutex_unlock(&lock); if (ret) printf("1:unlock%s
", strerror(ret)); #endif return NULL; } int main(void) { int ret; pthread_t tid[2]; pthread_attr_t attr; pthread_mutexattr_t mutexattr; pthread_attr_init(&attr); pthread_mutexattr_init(&mutexattr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE_NP); pthread_mutex_init(&lock, &mutexattr); pthread_mutexattr_destroy(&mutexattr); ret = pthread_create(&tid[0], &attr, test0, NULL); if (ret) { fprintf(stderr, "create:%s
", strerror(ret)); exit(1); } ret = pthread_create(&tid[0], &attr, test1, NULL); if (ret) { fprintf(stderr, "create:%s
", strerror(ret)); exit(1); } pthread_attr_destroy(&attr); pthread_exit(NULL); }

좋은 웹페이지 즐겨찾기