스레드 동기화
스레드의 동기화
저번 포스팅에서 배웠듯이 멀티 스레드 환경에서는 여러 스레드가 하나의 프로세스의 데이터, 코드를 공유하며 동시에 작동한다. 그러므로 공유되는 데이터에 접근하는 코드인 임계 구역(Critical Section)에 에 별다른 조치 없이 동시에 접근한다면 문제가 발생할 수 있다.
예를 들어 전역변수에 접근하여 수정하는 함수를 두 스레드가 동시에 호출한다면 전역 변수의 값이 어떻게 수정될 지는 아무도 모를 것이다. 그렇기에 우리는 동시에 스레드가 접근하더라도 하나의 스레드가 접근한 것과 같은 결과를 도출하는 스레드 안전성(Thread Safe)를 보장하기 위해 적절한 조치를 취해주어야 한다. 그 조치를 동기화(Synchronization)라고 한다. 동기화의 기법에는 뮤텍스 락, 세마포어, 모니터 등이 있다.
상호 배제(Mutual Exclusion)
줄여서 Mutex라고도 한다. 임계 구역에서의 스레드끼리의 경쟁에 대한 해결책 중 하나로 임계구역에 접근하는 스레드를 하나로 제한하는 것이다. 임계 구역에 접근한 스레드의 작업이 끝날 때 까지 다른 스레드가 진입하지 못하는 것을 보장한다. 상호 배제를 이루어내기 위한 기법들은 다음과 같다.
- 인터럽트 해제 : 임계 구역에 진입하면 해당 작업에 대한 모든 인터럽트를 무시하는 것이다. CPU를 독점하며 중요한 인터럽트도 받지 못하는 등 많은 단점이 있다.
- Busy-Waiting : 어떤 작업이 임계 구역에 진입하면 다른 작업이 계속해서 임계 구역이 비었는지를 확인하는 것이다. 당연히 기다리는 시간에 비례하여 오버헤드가 매우 커지게 된다.
- Semaphore : 위의 Busy-waiting 문제를 해결하기 위한 방법이다. 작업 대기큐와 semaphore lock을 준비하고 lock이 1 이상이면 임계 구역이 비어있다는 뜻이므로 lock을 1 감소시키고 임계 구역에 진입한다.
lock이 1 이상이 아니라면 임계 구역에 진입한 스레드가 있다는 뜻이므로 lock을 1 감소시키고 대기 큐에 진입, sleep한다. 임계 구역에서의 작업이 끝나면 lock을 반환, 1 증가시키고 lock이 0 이하라면 대기하고 있는 작업이 있다는 뜻이므로 signal을 발생시켜 sleep중인 대기 큐의 작업을 깨운다. 공중 화장실에서 줄 서있는 것을 연상하면 쉽다. - Mutex lock : 자물쇠 역할을 하는 변수 하나와 임계 구역을 잠그는 함수 하나가 있으면 된다. lock이 0이라면 임계 구역에 진입 후 lock을 1로 만들어 잠그고, lock이 1이면 Busy-waiting 한다.
lock이 0인지 확인하고 진입하는 두 과정이 atomic 해야만 lock을 0인지 확인하고 문맥 교환이 일어나 두 스레드가 모두 진입하는 문제를 막을 수 있다. - 모니터 : JAVA에서 사용되며 하나의 객체(데이터)마다 하나의 모니터를 결합하여 해당 객체에 두 개 이상의 스레드가 동시 진입할 수 없도록 한다. 각 모니터는 lock 역할을 하는 변수 하나와 OS에서 지원하는 조건 변수 하나를 가진다. 작동 과정은 다음과 같다.
1) 작업이 실행되기 전, 임계구역 객체의 모니터에서 Acquire 함수를 먼저 실행한다.
2) 임계 구역에서 이미 연산 중인 스레드가 있다면 lock이 true일 것이고, lock이 true라면 조건 변수 x의 x.wait() 함수를 실행한다.
3) OS가 객체에 연결된 대기 큐에 작업을 넣고 wait 시킨다.
4) 만약 lock이 true가 아니라면 lock을 true로 바꾸고 작업을 실행한다.
5) 작업 수행 후 모니터의 release 함수를 실행하여 lock을 false로 바꾸고 조건 변수 x의 x.signal 함수를 실행한다. x.signal 함수로 OS가 대기 큐의 wait 중인 스레드들을 모두 ready로 바꿔 ready queue로 보낸다. 그 후 CPU 스케줄러의 선택을 받아 ready queue의 작업이 실행된다.
지금까지 스레드 간의 상호 배제와 스레드 안전성을 보장하는 동기화 기법에 대해 배워보았다. 다음 포스팅에서는 Deadlock에 대해 배워보도록 하겠다.