해당 글은 기초만 정리한 내용입니다.
둘 이상의 프로세스 혹은 스레드가 동시에 접근해서는 안되는 Shared Resource(공유자원)을 접근하는 코드의 일부를 의미한다. 이 말은 즉, race condition이 발생할 수 있는 코드의 일부이다.
*race condition(경쟁상태) = 공유자원에 대해 여러 개의 프로세스(또는 스레드)가 동시에 접근을 시도할 때 접근 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태를 말함
간단하게 예를 보자.
두 명의 사용자가 프린터를 출력하기 명령을 내린다면, 각 인쇄는 중단되지 않고 한번에 실행되야 한다. 만약 프린터 드라이버가 두 사용자의 자료를 부분적으로 보내게 된다면, 예상하지 못한 결과를 초래하게 된다. 그러므로 프린터 드라이버는 하나의 프로그램으로부터 둘 이상의 명령이 오더라도 항상 예상된 결과대로 처리해줘야 한다. (원자성)
원자성이란?
*atomicity는 데이터베이스 시스템에서 ACID 트랜잭션 특성중의 하나다. 하나의 원자 트랜잭션은 모두 성공하거나 또는 실패하는 데이터베이스 운용의 집합이다. 원자성의 보증은 데이터베이스의 부분적인 갱신으로 더 큰 문제가 야기되는 것을 방지한다.
티켓 주문이 원장성의 한 예다. 티켓은 반드시 지불과 예약이 동시에 되거나 아니면 모두 되지 않아야 한다. 성공적으로 지불은 되었으나 좌석 예약은 되지 않은 경우는 허용되지 않는다. 하나의 트랜잭션은 티켓 예약뿐 아니라 운송, 환율 등..적용되는 곳이 많다.
이제 한 예제 코드를 보자
fun main() {
val test1 = Test()
val test2 = Test()
val test3 = Test()
test1.start()
test2.start()
test3.start()
}
class Test : Thread() {
private var count = 0
override fun run() {
count++
println(count)
}
}
이 코드를 예상한다면 1, 2, 3이 한번씩 출력되어야 한다. (순서는 알 수 없으니)
하지만, 실행해보면 같은 숫자가 호출되는 것을 볼 수 있을 것이다(1이 3번 나옴)
이 처럼 예상치 못한 결과를 초래하기 떄문에 동기화를 해줘야한다. 간단한 하게 lock을 획득해서 하는 방법등 이 있다.
렉이 하나뿐인 헬스장에 다닌다고 가정하자. 렉을 사용하기 위해서는 카운터에서 키를 받아서 가야한다. 내가 스쿼트를 하려고 하는데 카운터에 키가 있으면 렉이 비어있다는 뜻이고 나는 그 키를 이용해 렉 앞으로 가서 스쿼트를 할 수 있다. 내가 신나게 하체를 조지고 있는데 다른 사람이 데드리프트를 하려고 왔다. 하지만 이 사람은 아무리 데드리프트가 하고 싶어도 키가 없기 때문에 렉을 사용할 수 없다. 이 사람은 내가 스쿼트를 끝내고 나올 때까지 카운터에서 기다려야 한다. 곧이어 다른 사람도 데드리프트가 하고 싶어졌고 이 사람 또한 렉을 사용하기 위해서는 카운터에서 대기해야 한다. 이제 내가 스쿼트를 마치고 카운터에 키를 반납했다. 이제 기다리던 사람들 중 맨 앞에 있던 사람이 키를 받을 수 있고 키를 이용해 렉을 사용할 수 있다.
이것이 뮤텍스가 동작하는 방식이다. 렉을 이용하는 사람은 프로세스 혹은 쓰레드이며 렉을 공유자원, 키는 공유자원에 접근하게 위해 필요한 어떤 오브젝트이다. 즉, 뮤텍스 Key에 해당하는 어떤 오브젝트가 있으며 이 오브젝트를 소유한 프로세스 or 쓰레드 만이 공유자원에 접근할 수 있다.
렉이 여러개 있는 헬스장이다. 카운터에는 현재 사용할 수 있는 렉의 개수를 보여주는 전광판이 있다.
내가 렉을 사용하고 싶다면 카운터에서 사용할 수 있는 개수를 확인하고 1개 이상이라면 사용할 수 있는 개수를 하나 뺀 다음에 렉을 사용해야 한다. 그리고 스쿼트가 끝나면 사용할 수 있는 개수를 하나 더 해준다. 모든 렉에 사람이 있을 경우 사용할 수 있는 개수는 0이 되며 이때 렉을 사용하고 싶은 사람이 있다면 사용할 수 있는 개수가 1로 바뀔 떄까지 기다려야 한다.
이처럼 세마포어는 공통으로 관리하는 하나의 값을 이용해 상호배제를 달성한다.