이전 글(프로세스와 스레드)에서 기본적인 프로세스와 스레드의 차이 그리고 멀티스레드와 멀티프로세스의 차이를 알아봤다.
결국 한 프로세스내에서 스레드는 메모리를 공유하기 때문에 병렬 처리를 할 때, 속도가 메모리 사용 측면에서 강점을 가진다. 하지만, 스레드를 제대로 사용하기 위해서는 몇 가지를 제대로 고려해야한다.
개발자가 실행 순서를 정확히 알지 못한다
2개의 스레드가 존재하고, 각 스레드는 counter에 1을 더한 후 저장하는 작업을 수행한다. counter = counter + 1
은 사실 원자적으로
수행되지 않는다. 기계어로 바꾸어 보면, counter에 있는 값을 eax레지스터에 옮기고, eax레지스터 값을 1 증가시킨 후, eax값을 다시 counter에 넣는다. 이렇게 3개의 기계어의 조합으로 코드가 작동된다.
그런데 기계어를 실행하는 도중에 context switch가 일어난다면?
=> 위 사진처럼 counter의 값이 기댓값인 52가 아니라 51이 나온다. 스레드 별로 레지스터를 독립적으로 보유하고 있다. 스레드1에서 eax레지스터 값을 증가시켜 51이 되었지만, 그와 동시에 context switch가 발생하여 CPU제어권이 스레드2로 넘어갔다. 스레드2에서 3개의 기계어를 모두 수행하면, counter에는 51이 들어가 있다. 그 후 스레드1로 CPU제어권이 다시 넘어온 뒤, 스레드1의 eax레지스터에 있는 51을 counter로 옮기면 counter는 51이다.
=> 개발자는 counter가 52가 되기를 바랬을 것이다. 하지만 원하는대로 코드가 작동되지 않는다.
프로세스 내 주소 공간을 공유하기 때문에 critical section
이 존재하고 이로 인해 race condition
이 발생한다.
위 그림의 문제의 원인은 counter라는 변수를 스레드들이 공유해서 사용하고 있다는 것이다. 공유 변수에 접근하는 부분을 critical section
이라고 한다. 여러 스레드들이 critical section
에 접근할 때 발생하는 문제를 race condition
이라고 한다.
이 문제점들을 해결하기 위해서는 명령어들이 원자적
으로 실행되어야 한다. 이를 구현하기 위해서 lock
이라는 개념이 등장한다.
명령어의 원자성을 해치는 요소 중 하나가 interrupt
다. critical section
에 있을 때 interrupt를 받지 않도록 해버린다면?
=> 원자성을 보장할 수는 있다.
critical section
에 접근한다면, 스레드의 인터럽트 비활성화는 의미가 없다.lock이 되었는지 아닌지를 보관하는 flag
사용
lock이 걸리면, flag
1로 설정
lock을 가지지 않는 스레드가 CPU 제어권을 가졌을 때, flag가 1이라면 무한 루프를 돌면서 대기
lock을 해제하면, flag
0으로 설정
context switch
가 발생할 수도 있다!!spin-waiting
: while문을 사용해서 계속 대기하는 것이 비효율적이다!원자적으로 lock과 flag를 동기화할 수가 있다
이로써 단순히 flag만 사용했을 때 발생하는 문제점1을 해결할 수 있다. 하지만 여전히 spin-waiting
은 존재한다.
지금까지 구현한 lock을 평가해보자.
1. 정확성 : mutual exclusion
을 구현할 수 있다
2. 공평성 : spin-lock은 공평함을 보장하지 못한다.
3. 성능 : spin-lock은 CPU를 계속 사용하면서 대기하는 것이므로 성능 bad
fairness
를 보장하기 위해 등장!
원자적으로 값을 증가시킬 수 있다
lock을 호출하면 해당 스레드는 Fetch And Add
를 통해서 기존 티켓 넘버를 가지고 티켓 넘버를 1증가시키는 것을 원자적
으로 할 수가 있다! lock을 가졌던 스레드가 unlock을 하면 turn 값이 1씩 증가한다. 이때도 Fetch And Add
사용함. turn 값과 같은 티켓 넘버를 가진 스레드가 lock을 얻게 됩니다. -> 모든 스레드 실행 보장(fairness)
하지만 여전히 spin-waiting
이 발생하고 있다. -> OS의 도움 필요!
spin이 발생하면 다른 스레드들이 실행될 수 있도록 CPU제어권을 양보한다!
lock을 가진 스레드가 unlock을 할 때, 다음 lock을 얻을 스레드를 지정!
스레드가 lock을 얻지 못한다면 큐에 추가!
여전히 spin-waiting이 아예 없지는 않다. 하지만, 스레드들이 큐에 보관되기 때문에 spin-wait 할 때 쓰는 시간을 줄일 수 있다!
Semaphore
: Lock과 동작 원리는 같지만, sem이라는 변수를 0과 1이 아니라 그 이상의 정수로 설정할 수 있게 하여 하나 이상의 스레드가 critical section에 접근할 수 있게 한다
lock과 unlock을 사용하는 것이 아니라 sem_wait와 sem_post를 사용하여 sem의 값을 조절한다.
sem_wait
: sem 1 감소, sem <= 0
라면, 큐에서 대기
sem_post
: sem 1 증가
Lock과는 달리 Semphore는 락 해제의 주체와 락 획득의 주체가 같지 않을 수도 있다!