프로세스는 실행중인 프로그램으로, 운영체제로부터 자원을 할당받는 작업의 단위로서 디스크로부터 메모리에 올라가 CPU 자원을 할당받는 것을 말한다.
쓰레드는 프로세스의 실행 단위로, 하나의 프로세스 내에서 여러 개의 실행 흐름을 가진 쓰레드가 존재하며 프로세스의 자원이나 데이터(주소 공간)를 공유받는다.
각각의 쓰레드는 독립된 작업 수행을 위해 스택을 별도로 가지고 있는데, 이는 각각의 쓰레드가 독립적인 작업 수행을 하기 위해 독립적인 함수 호출이 필요하고, 이를 위해서는 독립적으로 사용할 수 있는 스택이 필요하기 때문니다.
또한 쓰레드는 마찬가지로 프로그램 카운터 레지스터를 독립적으로 가지고 있는데, 스케줄러에 의해서 CPU 할당을 받고 이후 특정 조건에 의해 선점당하게 되면 추후 실행 시 실행 흐름을 기록한 레지스터로부터 불러와야 하기 때문이다.
동기화를 적용하는 이유에 대한 예를 데이터 레이스(Data Race)로 들어보자.
만약 우리가 2개의 쓰레드에서 특정 변수에 1을 더하는 연산을 한다고 가정해본다.
global_count = 0;
void *p_function(void *data)
{
for(i = 0; i < 50; i++)
global_count = global_count + 1
}
int main(void)
{
pthread_t pthread[2];
int thr_id;
int status;
thr_id = pthread_create(&pthread[0], NULL, p_function(),NULL);
thr_id= pthread_create(&pthread[1], p_function(), NULL);
pthread_join(pthread[0], (void**) &status);
pthread_join(pthread[1], (void**) &status);
}
물론 50회를 더하는 C프로그램은 컨텍스트 스위칭이 일어나기 이전에 연산이 끝나겠지만, 만약 더하는 연산 과정에서 컨텍스트 스위칭이 일어난다고 가정해보자.
값을 더하는 연산은 위와 같이 레지스터에 특정 변수를 읽어오고, 가산 연산을 한 뒤, 다시 그 값을 저장한다. 이 때 만약 두 쓰레드 모두 호출 - 가산까지 정상 실행된 뒤 문맥 교환이 일어나고 다시 저장되는 연산을 한다고 가정해보다.
그렇게 되면 global 변수에 저장이 되지 않은채로 Thread 1에서 가산 연산이 마무리되었기 때문에 Thread 1에는 현재 global_count보다 1 더해진 값이 레지스터에 들어가있다. 이후 Thread 2에서 변수를 호출하는 과정에서 기존 Thread 1에서 연산해 놓았던 변수가 덮어씌워지고, 문맥 교환 이후 Thread 2 의 연산결과인 global_count + 1이 레지스터에 남아있다.
기존 실행흐름대로 Thread 1에서는 레지스터에 남아있는 값을 저장할 것이고, Thread 2로 문맥 교환이 일어난 뒤에도 마찬가지다. 그렇다면 여기서 각 쓰레드는 1을 더하고 저장을 하는 연산을 수행했지만, 한번의 덮어씌움으로 인해 사용자에게는 연산 한번이 수행되지 않은 것처럼 보여진다.
위와 같이 개별적인 쓰레드가 동일한 자원에 접근해서 동시에 수정할 시 각 쓰레드의 결과에 영향을 주는 동기화 이슈가 발생한다. 이를 해결하기 위한 방안으로 변수의 값을 변경하는 부분에 동기화 락을 걸어둘 수 있다.
임계 영역에 대한 접근을 막기 위한 락을 거는 메커니즘에는 크게 2가지가 잇다.
세마포어의 주요 명령으로는 P 연산, V 연산이 있으며 다음과 같다.
P(S): wait(S)
{
while S <= 0;
S--;
}
V(S) : signal(S)
{
S++
}
P 연산에서 Busy Waiting(바쁜 대기)을 하게 되면 대기 상태에서도 loop를 돌며 CPU의 점유율을 소모하기 때문에 이에 대한 보완책으로 대기 큐 기법을 사용한다. 그 방식은 다음과 같다.
바쁜 대기: 임계 영역(critical area)에서 작업 중인 스레드 B를 기다리는 스레드 A가 있음. 이때, A는 B가 끝날 때까지 즉, 임계 영역에 들어갈 수 있을 때까지 아무 작업도 하지 않고 임계 영역에 접근이 가능한지 무한으로 검사만 하고 있는 현상
P(S): wait(S)
{
S -> count--;
if(S -> count < 0)
{
add thus process to S-> queue;
block();
}
}
V(S) : Singal(S)
{
S -> count++;
if(S->count < 0)
{
remove a process P from S->queue;
wakeup(P);
}
}
이전 P 연산 구현과 다르게 S -> count를 먼저 1 감소시키고, 만약 이 때 S -> count가 음수가 되면 현재 쓰레드를 Block시켜 대기하게 만들고,이후 접든 가능 쓰레드 수가 증가하게 되면 두 가지 상황이 발생한다.
POSIX 세마포어를 사용하기 위해 호출하는 라이브러리는 다음과 같다.
#include<semaphore.h>
#include<pthread.h>
sem_t semaphore;
typedef struct{
struct _pthread_fastlock __sem_lock;
int __sem_value;
_sem_lock_t __sem_waiting;
} sem_t;
POSIX 세마포어에는 2가지 종류의 세마포어가 있는데,Named 세마포어와 Unnamed 세마포어가 그것이다. 주요 세마포어 함수들은 다음과 같다.
먼저 Named 세마포어 생성을 위한 함수이다.
sem_t* sme_open(const char *name, int oflag, mode_t mode, unsignd int value);
int sem_unlink(const char *name);
Unnamed 세마포어 생성을 위해서는 다음 2개의 동작이 필요하다.
sem_t sem_id;
int sem_init(sem_t *sem_id, int pshared, unsigned int value);
다음은 세마포어의 P 연산에 해당하는 함수이다.
sem_wait(sem_t *sem);
sme_trywait(sem_t *sem);
sme_timedwait(sem_t *sem, const struct timespec *abs_timeout);
다음은 세마포어의 V연산에 해당하는 함수이다.
sem_post(sem_t *sem);
세마포어를 전부 사용한 경우,Named 세마포어는
sem_close(sem_t *sem);
함수를 통해서,
Unnamed 세마포어는
sem_destroy(sem_t *sem);
함수를 통해서 종료한다.
무한 대기 상태로 두 개의 작업이 서로 상대방의 자원이 해제되길 기다리고 있어 다음 단계로 진행하지 못하는 상태. 쓰레드와 프로세스 모두 이러한 상태가 발생될 수 있다. 이러한 상태를 데드락이라고 한다.
교착 상태의 발생조건은 다음 조건을 모두 충족시켜야 한다.
데드락을 예방 혹은 회피하기 위해서는 위 4개의 상황 중 1개의 상황만 발생하지 않도록 하는 것으로 이를 해결할 수 잇다. 다만, 위 조건을 방지하는 것으로 데드락을 예방하기 위해서는 시스템의 자원이 매우 많이 소모되므로 그 효율성이 떨어진다는 단점이 있다.
f
위처럼 예방 및 회피방법을 사용하지 않는다면 데드락에 대한 탐지와 획복을 하는 방식으로 데드락을 처리하는데,Allocation(자원 할당 그래프),Request,Available 등으로 데드락 여부를 탐색하고, 교착상태에 빠진 프로세스를 중단시키거나 자원을 선점하도록하여 회복하는 방법을 이용한다.
특정 프로세스의 우선순위가 너무 낮아 원하는 자원을 계속 할당받지 못하는 상태