이번 글에서는 뮤텍스와 세마포어에 대해 알아볼게요
뮤텍스는 한 번에 하나의 스레드만 특정 코드 영역에 들어올 수 있도록 막아주는 동시성 제어 도구로 주된 목적은 여러 스레드가 동시에 하나의 데이터를 수정하면서 발생하는 문제, 즉 데이터 깨짐(Race Condition)을 방지하는 것이에요
동작 방식은 다음과 같아요. 어떤 스레드가 lock()을 호출하면 해당 구간에 대한 사용권을 얻고, 다른 스레드들은 이 구간에 들어오지 못한 채 기다리게 됩니다. 이후 작업을 마친 스레드가 unlock()을 호출해야만 대기 중이던 다음 스레드가 실행될 수 있죠. 중요한 점은, 이 잠금을 건 스레드만이 반드시 해제할 수 있다는 소유권 개념이 있다는 것이이에요.
이를 이해하기 쉽게 비유하면, 화장실이 한 칸뿐인 상황과 같아요. 한 사람이 들어가서 문을 잠그면 다른 사람들은 밖에서 기다려야 하고, 안에 있던 사람이 나와서 문을 열어줘야만 다음 사람이 들어갈 수 있죠. 즉, 뮤텍스는 “한 번에 한 명만 접근 가능하게 하여 데이터를 안전하게 보호하는 장치”라고 보면 됩니다.
세마포어는 한 번에 여러 개의 작업이 동시에 실행될 수 있도록 허용하면서도, 그 개수를 제한하는 데 목적이 있는 도구에요. 내부적으로는 카운터라는 값을 가지고 있으며, 이 값이 현재 추가로 들어갈 수 있는 남은 개수를 의미해요. 예를 들어 초기값이 3이라면, 동시에 최대 3개의 스레드가 특정 작업을 수행할 수 있는거죠.
스레드가 작업을 시작할 때는 wait()을 호출하면서 카운터를 1 감소시키고, 작업이 끝나면 signal()을 호출하여 다시 1 증가시켜요. 만약 카운터가 0인 상태에서 추가로 wait()이 호출되면, 해당 스레드는 다른 스레드가 signal()을 호출할 때까지 대기하게 돼요. 여기서 중요한 점은 뮤텍스와 달리 소유권 개념이 없기 때문에, 어떤 스레드든지 signal()을 호출할 수 있다는 점이에요.
이를 비유하면, 화장실 예를 들어 3칸이 있다면 동시에 3명까지는 사용할 수 있고, 네 번째 사람부터는 자리가 날 때까지 기다려야 합니다. 누가 먼저 나오는지는 중요하지 않고, 자리가 하나라도 비면 다음 사람이 들어갈 수 있죠. 즉, 세마포어는 “동시에 실행되는 작업의 개수를 조절하여 시스템 과부하를 막는 장치”라고 이해하면 돼요.
소유권이 있는 뮤텍스와 달리 세마포어는 없잖아요? 그러면 대기중이던 스레드가 signal()을 호출할 수도 있다는 말이에요. 하지만 대기중이던 스레드가 signal()을 호출 안하게끔 해야 한대요. 이에 대해 알아볼게요
세마포어는 내부적으로 동시에 접근 가능한 갯수를 나타내는 카운터가 있어요. 그런데 이 카운터는 물리적 제한이 아니고 개발자가 정해둔 논리적 제한 갯수를 의미해요. 즉 카운터가 3이라 해도 실제로는 4개 이상의 작업이 진행될 수도 있죠. 그런데 “동시에 3개까지만 하자”라고 규칙을 만들었고 그 규칙을 숫자로 관리하는 도구가 세마포어에요.
세마포어는 뮤텍스와 달리 소유권이 없어서 대기중이던 작업들도 signal() 호출이 가능하다 했죠? 대기중인 작업들은 실제 작업중이던 작업들이 종료됐는지 알 수 있는 방법이 없어요. 그런데 호출할 경우 카운터가 1이 증가하게 되겠죠? 이때 진행중인 작업이 3개인 경우 카운터가 1이 늘어나서 처음 약속과 달리 4개의 작업이 진행되게끔 되는거죠. 이러한 경우를 방지하기 위해 signal은 작업을 하러 들어간 스레드가 작업을 끝난 후에 호출하도록 코드를 작성해야 해요.