Computer Science/시스템 프로그래밍

[시스템 프로그래밍] Chapter 13

seungwon9201 2024. 6. 10. 16:45

Mutex : 상호 배제라는 뜻으로 mutual exclusion의 약자이다. 스레드하나가 임계영역에 들어오면 다른 스레드들은 들어오지 못하도록 배제하는 것

mutex 변수를 이용해서 여러 공유 데이터들을 동시에 액세스 하는 문제점을 보호할 수 있다. 

mutex변수가 Lock과 같은 개념으로 작동할 수 있다. 

 

즉, Mutex라는것의 기본 개념은 오직 하나의 스레드만 mutex변수(lock)를 소유할 수 있다. 그래서 여러 스레드들이 동시에 액세스 한다면 가장 먼저 요청한 스레드에게만 lock을 주고 다른 스레드들은 대기한다. 그래서 lock을 다시 용하면 lock을 해제해야 한다. 그래야 다음 스레드들이 액세스 할 수 있기 때문이다. 스레드들은 반드시 임계영역에 의해서 보호된 데이터를 액세스 하는 순서를 가져야 한다.(starvation문제를 해결) 

 

같은 계좌에 대해서 동시에 엑세스할떄 이러한 문제점이 발생할 수 있음
producer는 값을 하나 증가시키고 싶은 거고 consumer는 값을 하나 감소시키는 스레드이다. 두개의 스레드가 동시에 작동한다면 이러한 문제사항이 발생할 수 있음.

그래서 이러한 문제를 해결하기 위해서 동시에 엑세스하지 못하도록 해야 한다. 

 

전역변수를 여러개의 스레드가 액세스 한다면 여러 스레드들이 액세스 하지 못하도록 임계영역으로 만들어줘야 한다. OS에서 제공해 주는 locking메커니즘 system call함수를 이용해서 하자. 

뮤텍스 변수를 선언하고 객체를 만들고 초기화를 하자. 그리고 뮤텍스변수에 대해서 lock을 요청을 해야한다. 모든 스레드가 동시에 요청하면 가장먼저 요청한 스레드만 lock을 받는다. 그리고 lock을 가진애들은 임계영역에서 테스크를 수행한다. lock을 얻지못한 애들은 대기한다. 임계영역에서 빠져나오면 unlcok을 해서 lock을 해제해야한다. 그러면 OS가 queue에서 기다린 다음애가 lock을 받는다. 마지막엔 mutex변수는 파괴해서 메모리 낭비가 되지 않도록 한다.

이런 식으로 사용하는 것은 프로그래머의 책임이다.(즉, 동기화 작업을 알아서 잘해라)

 

pthread_mutex_t의 변수는 뮤텍스 lock을 표현할 수 있는 변수고 초기화를 해야한다. 뮤텍스 변수를 static하게 변수를 선언하면서 초기화하는 방법은 constant값은 PTHREAD_MUTEX_INITIALIZER로 하는 방법이 있다. 또한 뮤텍스 변수를 dynamically allocated변수로 선언했을 경우에 그러한 변수를 초기화하는 방법은pthread_mutex_init()함수를 호출하는 것이다. 성공시 0 에러시 에러코드가 바로 리턴된다. 초기화작업은 변수선언하고 한번만 하면되는데 중간에 한번더 호출해서 초기화한다면 따로 정의된것이 없기 떄문에 초기화는 한번만 하는 것이 좋다.
mutex변수를 다 쓴줄알고 파괴했는데, 알고보니 다 사용한것이 아니였다. 이럴경우엔 오류가 발생하니. 파괴하기전에 확인을하자. mutex를 사용중인데도 파괴해서는 안된다.
mutex_lock으로 lock을 건다. 임예영역에서 task를 다하면 unlock을 호출해서 소유권을 해제하자. 그래서 자연스럽게 lock과unlock사이는 임계영역이 된다. trylock은 lock을 얻을 수 있는지 시도해보는 함수이다. 즉, mutex_lock은 blocking버전이고 mutex_trylock은 nonblocking버전이다. 예제처럼 선언을 하고 바로 초기화를 한다.
함수가 여러번 실행이 되더라도 함수가 실행이 되는 것은 최초에 한번만 실행이 되게끔 할 수 있는 매커니즘을 지원해주는것이 pthread_once함수이다. at-most-once execution이란 한번만 실행이 되어야 하는 것이다.

 

그러나 이 함수는 초기값을 파라미터로 넘겨주기 위해서 사용할 수는 없다.
초기값을 파라미터로 넘겨주기 위해서 다른 대안임. mutex를 static하게 초기화를 하자. pthread_once함수를 사용하지않고 스태틱변수의 초기화를 이용해서 그리고 flag변수를 이용해서 임계영역 내부를 한번만 실행하도록 구현한 예시
이번엔 적어도 한번 실행이되는 것이다. 만약에 다른 스레드를 만든다면 mutex 변수를 딱한번(exactly once)만 초기화해야한다.


동기화는 mutex 말고 또 다른 방법이 있다. Condition variables 

locking 매커니즘으로 동기화를 하다 보니 또 다른 동기화 요구사항이 생겼다. 그래서 나오게 된 것이 condition variables이다. 조건은 임계영역 안에서 또 다른 동기화가 필요할 때 사용한다.

 

 

임계영역안에서 스레드가 task를 수행해야하는데 task를 수행할 조건이 만족되지 않아서 task를 수행하지 못할경우에 조건이 만족될때까지 기다려야한다. 이러한 기다림은 비효율적이기때문에 특정조건이 만족될때까지 임계영역안에서 busy waiting 을 사용하지 않고 임계영역안에서 기다리는 매커니즘이 필요하게 되었다. 그래서 조건변수라는 매커니즘은 일단 lock을 얻어야한다. x,y가 같으면 그대로 작업하지만 다를경우에 스레드를 suspend시키고 다른 함수가 임계영역에 오도록 mutex를 unlock한다. 이런식으로 공유변수를 변경하고 꺠워주는 스레드, 조건을 검사하고 맞지않으면 wait하는 스레드의 동작을수행하기 위해서 condition variable이 필요하다.

 

condition variables이라는 것은 또다른 종료의 스레드들 간에 동기화를 통해서 실행이 될 수 있는 매커니즘이 될 수 있다. mutex도 여러 스레드가 공유데이터에 동시에 엑세스하는 것을 막기 위한 또 다른 동기화 매커니즘이었다. codition variables은 어떤 공유변수의 데이터값을 비교해서 조건이 맞는지 확인을 하고 기다리는 스레드, 또다른 스레드는 공유 데이터의 갑을 변경시키고 대기중인 스레드를 깨워주는 스레드 이런 스레드의 동작 동기화를 위해서 condition variables에대해서 wait하는 것과 wait한것을 깨워주는 오퍼레이션이 필요하다. condition variable이 없다면 개발자는 조건이 맞을때까지 기다리는 busy wait를 해야만한다. 이러한 방법은 리소스를 낭비하니까 condition variable이 나오게 되었다. condition variable은 항상 mutex lock과 함꼐 사용한다.
기다리는 wiat와 깨우는 signal이 있다. 이 함수를 호출하기 위해선 반드시 mutex lock을 얻고난 다음에 호출해야한다. pthread_cond_signal함수는 대기중인 스레드를 적어도 하나 꺠우는 함수이다. 이 함수는 condition variable에서 대기중인 스레드를 mutex의 queue로 이동시키는 역할을 한다.

 

두코드는 물리적으론 떨어져있지만 사실 같은 섹션으로 작동한다.



condition variable을 사용하기 위해서는 mutex와 마찬가지로 condition variable타입의 변수를 선언하고 초기화를 한 다음에 사용해야한다. mutex와 사용양식이 동일하다. 초기화 함수를 통해서 초기화 하든지 스태틱 하게 초기화하는 방법이 있다. mutex와 마찬가지로 초기화한것을 또 초기화 해서는 안된다.
다 사용한 객체는 파괴하자. 더 이상 다른 스레드가 condition variable을 사용하지 않는 것을 확인한 위에 삭제하자.

 

condition variable을 초기화한 뒤에 사용하기 위해서 wait와 signal이 있다. wait함수를 호출하면 첫번째 파라미터인 onditional varaiable에 대해서 호출한 스레드가 suspend가 되어서 condition variable의 waitung queue에 들어간다. 그와 동시에 두번쨰 파라미터에 블락 시키고 있었던 mutex를 언락시킨다. 왜냐하면 내가 잠들고 난 다음에 임계영역에 다른 스레드들이 들어오도록 하기위해서이다. timedwait함수도 두개의 파라미터는 동일하다. 세번재 파라미터가 추가적인 timeout값을 지정하고 싶을떄 사용한다. 그래서 timedwait함수는 시간이 만료가되면 꺠어나는 추가적인 시간옵션을 지정할 수 있는 차이점이 있다.

 

스레드를 꺠워주는 signal함수도 두가지가 있다. pthread_cond_signal함수를 호출하면 이 함수의 파라미터로 넘긴 condition variable의 waiting queue에 대기중인 스레드들 중에서 적어도 하나를 깨운다. 깨운다라는 것은 condition variable의 waiting queue에서 꺼내서 unblock했던 mutex의 waiting queue로 다시 넣는다는 것이다. 그래서 꺠어난 스레드는 일단 자기가 놨던 mutex를 먼저 락을 가진 상태에서 wait 함수를 리턴할 수 있게된다. broadcast는 waiting하고 있는 모든 스레드들을 다 꺠운다는 것이다.


Signal handling and threads

 

프로세스안에 있는 모든 스레드들은 프로세스의 시그널 핸들러를 공유한다. 각각의 스레드들은 각각의 signal mask를 가질 수 있다. 다중 프로세스에서 시그널이 왔을 때 이 시그널은 어떤 스레드가 받아야 할까에 관한 delivery 메커니즘에 따라서 전달되는 방식을 3가지로 나눌 수 있다. 

Asynchronous : 시그널 마스크로 막지 않은(unblock한) 스레드에게 전달되는 경우, 그래서 받고자 하는 시그널이 여러 개면 여러 개로 시그널이 전달된다. 

Synchronous : 동기식으로 전달하는 방식으로 그 시그널을 발생시킨 스레드에게 시그널을 전달하는 방식이다.

Directed : 누군가가 타깃시그널을 지정한다면 그 특정 스레드에게 전달되어야한다(이미 시그널 안에 목적지 스레드가 정해져 있다는 뜻)

 

 

특정 스레드에게 시그널을 보내기 위해서 pthread_kill함수를 이용하자. 첫번째 파라미터에는 타깃 스레드를 정하고 두번째 파라미터에는 어떤 신호를 보낼지 정한다.
스레드별로 signal mask를 제어하기 위한 함수로 pthread_sigmask함수가 있다. 동작과정은 sigpromask함수의 파라미터와 동일하다. 첫 번째 파라미터는 시그널 마스크를 어떻게 변경할건지 정하는것이다(3가지) SIG_SETMASK로 한다면 기존에 저장된 시그널들을 무시하고 두번째 파라미터로 지정한 signal set들로 signalmask를 다시 설정한다. SIG_BLOCK인경우 signal mask에 있는 시그널들을 그대로 두고 두번째 파라미터로 지정한 시그널셋안에 시그널들을 추가하는 의미이다. SIG_UNBLOCK은 반대로 현재 signal mask에 있는 시그널중에서 두번째파라미터로 지정한 시그널안에 있는 시그널들을 빼는 작업이다. 즉, 현재 signal mask에서 시그널을 빼고 싶을떄 사용한다.

 

다중 스레드가 존재하는 프로세스에서 시그널 핸들링을 해야할필요가 있을 때 아래의 예제를 이용해서 처리하자.

메인스레드가 일단 모든 시그널을 블록을 시키고 시작하자. 그리고 시그널을 전담할 dedicated스레드를 만든다. sigwait함수가 리턴이 되면 pending리스테서 그 시그널을 삭제하고 sigwait함수가 리턴이 된다. 또다른 방법으로 pthread_sigmask으로 unblock할수도 있다.


Readers and writers 

 

여러 스레드가 동시가 공유 리소스를 write한다면 충둘문제가 발생할 수 있어서 임게영역을 두었는데 read를 호출할떄는 임계영역으로 설정하면 오히려 퍼포먼스가 떨어질 수가 있다. 그래서 오퍼레이션에 따라서 lock을 주는것을 reader-writer lock이라고 한다. mutex lock보다 좀더 유연성이 있다. reader 오퍼레이션으로 요청할 수 있고 writer 오퍼레이션으로도 요청할 수 있다. reader 용으로 요청한다면 하나의 스레드에게 락을 주는것이 아니라 여러스레드에게 락을 줄 수 있다. 이렇게 두가지가 있다보니 reader, writer중에서 누구를 먼저 lock을 주어야하냐에 관한 질문이 생길 수 있다. reader와 writer가 같이 있으면 reader에게 먼저 읽으라고 lock을 준다. reader가 다 읽고 writer에게 준다. 이것이 strong reader synchronization방식이다. 반대로 둘이 같이있을떄 writer에게 먼저 락을 주어서 쓴다음에 엑세스하라는 것이 strong writer synchronization이다.

 

read-write타입의 변수를 선언하고 초기화해야하는것도 다른 매커니즘과 동일하다. 여기도 동일하게 초기화를 한번만 해야한다.
다 사용한 read,write객체는 파괴해서 리소스 낭비를 줄여주자. 이것도 동일하게 지운상태로 변수를 참조한다면 정의되어있지 않아서 어떤일이 발생할 지 알수 없다.
relock은 read용 lock을 blocking모드로 요청하는 함수이다. trydlock은 read용 lock을 non blocking모드로 요청하는 함수이다. write도 이와 동일, unlock함수는 언락하기 위해서 사용하는 함수

mutex lock과 read-write lock을 사용하는 것을 비교하면 read-write lock을 쓰는 것이 오버헤드가 더 있을 수 있다. 왜냐하면 read용인지 write용인지에 따른 부가적인 처리가 필요하기 때문이다. 반면에 read 오퍼레이션만 수행하는 스레드가 lock을 요청한다면 mutex보다 좀 더 유연하게 동시에 공유변수를 액세스 할 수 있도록 하면서 퍼포먼스를 향상할 수 있다. 


A strerror_r omplementation

 

strerror()

이 함수는 thread safe하지 못해서 충돌 문제가 발생했었다. 그래서 보호하기 위해서 mutex lock을 이용해서 안전하게 호출할 수 있도록 할 수 있다. 같은 방법으로 perror도 안전하게 만들 수 있다. 그리고 이 두 함수는 thread safe 하지도 않고 async-signal safe 하지도 않다. 즉, 시그널 핸들러 안에서는 호출하면 안 되는 함수이다. 그래서 이 함수들을 async-signal safe 하게 만들기 위해서 이 함수가 수행되는 동안 시그널을 잠시 block 시킨다. 이렇게 하기 위해서 sigpromask를 이용한다. 


Deadlock

 

스레드가 mutex lock을 갖고있는 상황에서 또 같은 변수에 관해 mutex lock을 요청하면 deadlock에 빠지게 된다. 또한 lock을 가진 스레드가 임계영역에서 작업을 하다가 에러를 만나는 경우에 에러메시지를 출력하고 리턴해버리는 경우가 에러핸들링 루틴에 많이 사용이 된다. 이때 개발자들은 unlock을 하고 리턴을 해야 하는데 lock을 갖고 있는 채로 리턴을 하는 경우가 있다. 이런 경우에는 다른 스레드들은 계속 기다리는 deadlock상황이 발생할 수 있다. 

결론은 다중스레드로 작성을 하면 발생할 수 있는 deadlock 문제를 포함해서 발생할 문제를 분석해서 잘 사용하자.