일단, 내가 제목에 적어놓은
뮤텍스, 스핀락, 세마포어를 사용하기 전에 어떤 상황인지 알아야 한다.
바로
비동기적인 프로세스들이 병행적으로 수행될때, 공유자원에 동시에 접근할 때 문제가 발생하고.
쉽게 말해,
서로가 서로에 대해 모르는 프로세스들이 동시에 수행되는데,
해당 프로세스들이 수행되며 필요한 자원이 겹치고, 해당 자원에 동시에 접근하려고 할 때 문제가 발생한다.
이러한 문제가 발생했을 때
OS가 사용하는 방식이 뮤텍스, 스핀락, 세마포어이다.
현실에서는, 아래의 사진과 같다.
서로가(프로세스가) 프린터(공유자원을)를 사용한다고 이야기하는 것이다.
그런데, OS에서도 그럴까?
막 프로세스끼리 다투고 싸워서 공유자원을 얻어내는것일까?
당연히 아니다..ㅋㅋㅋㅋㅋ
서로 다른 프로세스나 스레드가 동시에 공유자원에 접근할때,
OS는 어떻게 처리하는지에 대한 내용이
뮤텍스, 스핀락, 세마포어 이다.
쉽게 설명하자면, 위와 같다.
왼쪽은 뮤텍스, 오른쪽은 스핀락이다.
위의 지금 가능? 은 지금 공유자원에 접근할 수 있는지를 나타낸다.
뮤텍스 방식
뮤텍스는 현재 사용중인 공유자원에 대한 접근을
대기큐(위의 잠깐 쉬어야지..)를 사용한다.
임계영역에 프로세스나 스레드가 있을 경우,
다른 프로세스나 스레드가 공유자원을 사용하려고 한다면
블록시키고 대기 큐에 넣어둔다.
스핀락 방식
스핀락은 현재 사용중인 공유자원에 대한 접근을
계속해서 스핀(불가능 -> 지금가능? -> 불가능 -> 지금가능?)하며 기다린다.
그래서, 대기중인 프로세스나 스레드는 계속해서 공유자원에 접근가능한지 여부를 알아야 하므로, (busy wainting이라고 한다)
뮤텍스보다 오버헤드가 크다.
하지만! CS에서 무조건 이방식이 옳다! 라는 말은 없듯,
마찬가지로
프로세스가 공유자원을 사용하는 상황을 보고 뮤텍스나 스핀락 방식을 사용해야 한다.
예를 들어,
뮤텍스 방식은 대기중인 프로세스를 대기 큐에 넣어놓는다.
CPU의 레지스터로 오려는 프로세스를 메모리상에 적재시켜놓아야 하므로,
콘텍스트 스위칭 오버헤드가 발생한다.
여기서 잘 생각해보자.
만약,
스핀락 방식처럼 프로세스가 계속해서 공유자원 접근을 시도하는 자원 소모가
뮤텍스 방식의 프로세스를 대기 큐에 넣어놓는 자원 소모보다 적다면?
그렇다. 이럴 때는 스핀락 방식이 유리하다.
또한, 공유 자원을 여러 프로세스가 접근할 수 있도록 만드는 멀티 프로세스 환경이라면?
이럴 때도 스핀락 방식이 유리하다. (공유자원 사용이 금방 이루어질 수 있으므로 대기 큐보다 계속해서 접근 시도가 나을 수 있다.)
정도를 고려하면 된다.
잘 보면, 위에서의 이야기는 공유자원이 하나일때를 이야기했다.
세마포어는,
공유자원이 여러개일때
다수의 프로세스나 다수의 스레드가 접근하는 방식이다.
쉽게 말해, 위의 뮤텍스와 스핀락을 다음과 같이 설명하겠다.
되게 맛있는 맛집이 있을때,
맛집의 자리는 딱 하나라서 한명씩만 받을 수 있다.
그런데 여러 손님이 왔을때는,
뮤텍스 방식은 가게에 들어온 한분 빼고 나머지 분들을 대기석에 앉혀놓는다.
스핀락 방식은 나머지 분들이 들어갈 자리가 있는지를 계속해서 가게에 물어보는것이다.
여기서, 가게의 자리가 늘어나는것이 세마포어 방식이다.
그렇다면 한가지 의문이 들 수 있다.
자리가 몇개 남았는지(사용 가능한 공유 자원의 수)를 알아야 다음 손님(대기중인 프로세스)을 받을 수 있다.
그렇기 때문에 세마포어는 내부 로직이 있다.
해당 내용에 대한 해결은 다음과 같다.
들어가는 손님은 P함수
(공유자원에 진입할 때)
나가는 손님은 V함수
(공유자원 사용을 중지할때)
를 사용해서, 현재 해당 공유 자원의 가용 여부를 알 수 있게 해준다.
사진은 다음과 같다.
여러개의 스레드(여러 손님)가 여러개의 공유자원(여러 자리)를 차지할 수 있다.
acquire, 즉 공유자원에 진입할때 P함수를 사용하고,
release, 즉 공유자원 사용을 중지할때 V함수를 사용해서
현재의 상황을 알 수 있게 해준다.
만약 세마포어 상황에서 모든 공유자원이 사용중일때는,
위에서 말해준
뮤텍스나 스핀락 방식 중 적절한 내용을 사용하면 된다.
실제로 OS에서 사용하는 c++ 코드는 다음과 같다.
#include <windows.h>
#include <stdio.h>
#define MAX_SEM_COUNT 10
#define THREADCOUNT 12
HANDLE ghSemaphore;
DWORD WINAPI ThreadProc( LPVOID );
int main( void )
{
HANDLE aThread[THREADCOUNT];
DWORD ThreadID;
int i;
// Create a semaphore with initial and max counts of MAX_SEM_COUNT
ghSemaphore = CreateSemaphore(
NULL, // default security attributes
MAX_SEM_COUNT, // initial count
MAX_SEM_COUNT, // maximum count
NULL); // unnamed semaphore
if (ghSemaphore == NULL)
{
printf("CreateSemaphore error: %d\n", GetLastError());
return 1;
}
// Create worker threads
for( i=0; i < THREADCOUNT; i++ )
{
aThread[i] = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE) ThreadProc,
NULL, // no thread function arguments
0, // default creation flags
&ThreadID); // receive thread identifier
if( aThread[i] == NULL )
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
// Wait for all threads to terminate
WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);
// Close thread and semaphore handles
for( i=0; i < THREADCOUNT; i++ )
CloseHandle(aThread[i]);
CloseHandle(ghSemaphore);
return 0;
}
DWORD WINAPI ThreadProc( LPVOID lpParam )
{
// lpParam not used in this example
UNREFERENCED_PARAMETER(lpParam);
DWORD dwWaitResult;
BOOL bContinue=TRUE;
while(bContinue)
{
// Try to enter the semaphore gate.
dwWaitResult = WaitForSingleObject(
ghSemaphore, // handle to semaphore
0L); // zero-second time-out interval
switch (dwWaitResult)
{
// The semaphore object was signaled.
case WAIT_OBJECT_0:
// TODO: Perform task
printf("Thread %d: wait succeeded\n", GetCurrentThreadId());
bContinue=FALSE;
// Simulate thread spending time on task
Sleep(5);
// Release the semaphore when task is finished
if (!ReleaseSemaphore(
ghSemaphore, // handle to semaphore
1, // increase count by one
NULL) ) // not interested in previous count
{
printf("ReleaseSemaphore error: %d\n", GetLastError());
}
break;
// The semaphore was nonsignaled, so a time-out occurred.
case WAIT_TIMEOUT:
printf("Thread %d: wait timed out\n", GetCurrentThreadId());
break;
}
}
return TRUE;
}
https://learn.microsoft.com/ko-kr/windows/win32/sync/using-semaphore-objects?source=recommendations
뮤텍스 방식과 스핀락 방식은 현재 대기중인 프로세스가 어떻게 대기하는지에 대한 방식이다.
세마포어 방식은 여러개의 프로세스, 여러개의 스레드가
여러개의 공유자원에 어떻게 접근하는지에 대한 방식이다.
레퍼런스
https://www.youtube.com/watch?v=oazGbhBCOfU
https://www.youtube.com/watch?v=AnYN-kbCbRI&list=PLBrGAFAIyf5rby7QylRc6JxU5lzQ9c4tN&index=18