같은 프로세스에 속하는 스레드는 메모리를 공유한다
프로세스는 서로 겹치지 않게 독립적인 메모리 주소 공간을 부여받기 때문에
따로 shared memery 형태로 쓰지 않는 이상 같은 메모리 공간에 접근할 일이 없어서 같은 데이터에 동시에 접근할 일이 없다
하지만 스레드의 경우에는 얘기가 달라진다
하나의 프로세스가 여러 개의 스레드들을 가질 수 있는데 이때 이 스레드들은 같은 부모 프로세스 소속이기 때문에
메모리 공간 또한 부모 프로세스의 메모리 공간을 공유한다
이렇게 하면 장점은
같은 부모 밑에 있는 스레드들은 따로 추가적인 조치를 해줄 필요 없이
스레드들 사이에서 데이터 공유가 가능해진다
하지만 이로 인해 필연적으로 조심해야 할 부분이
여러 스레드들이 같은 데이터에 접근하다보면 예상했던 것과 다른 결과가 발생할 수 있다는 것이다
그리고 이 지점에서 락(lock)의 필요성이 생기게 된다
락이 없을 때 생기는 문제
예를 들어 보겠다
프로세스 A는 스레드 x와 스레드 y를 가지고 있다
스레드 x와 y가 데이터 D에 접근을 해서 둘 다 1씩 증가시켜준다면 어떻게 될까?
프로그래밍 언어로는 단순하게
D++;
로 작성하겠지만 이 명령문이 CPU 레벨에서 실행되는 명령문들로 바뀌게 되면
하나의 문장이 아니라 아래와 같이 복수의 명령문들이 된다 (디테일은 다를 수 있음, 지금은 이해하기 쉽게 표현)
LOAD D to R1
(메모리에서 CPU 레지스터1로 D 값을 로딩)
R1 = R1 + 1
(레지스터1의 값에 1을 추가해서 다시 레지스터1에 씀)
STORE R1 to D
(레지스터1의 값을 메모리에 있는 D의 값으로 저장)
그렇다면 스레드 x와 y가 동시에 D++; 를 실행하면 어떤 현상이 생길 수 있을까?
다양한 현상들이 생길 수 있는데 아래는 그중에 하나의 현상이다
(D의 초기 값은 0이며, 괄호 안에 있는 값은 명령문이 실행될 때의 값이라고 하겠다)
스레드 x | 스레드 y |
LOAD D(0) to R1 | |
LOAD D(0) to R1 | |
R1 = R1(0) + 1 | |
R1 = R1(0) + 1 | |
STORE R1(1) to D | |
STORE R1(1) to D |
스레드 x와 y가 모두 D++; 를 했기 때문에 기대하는 최종 결과는
D는 2가 되는 것이지만
위 처럼 동작하게 되면 D의 최종 값은 1이 된다
이 문제를 해결하기 위해서 락이 등장한다
락(lock)은, 여러 스레드가 공유해서 쓰는 데이터를 보호할 때 사용된다
락을 쥔 스레드만 그 이후의 동작을 수행할 수 있고
동작 수행이 완료되면 그 스레드는 락을 반환해서 다른 스레드들이 락을 쥘 수 있도록 해야 한다
락을 쥐고 나서부터 락을 해제하기 전까지의 영역을 critical section(임계 영역)이라고 하며
이 영역은 일반적으로 하나의 스레드만 접근 가능하기 때문에 mutual exclusion(상호 배제)이라는 특징을 가진다
아까 예제의 문제를 락을 써서 해결해 보자
그럼 위의 문제를 락을 통해 해결해 보자
L 이라는 이름의 락을 하나 쓰기로 했다
아래와 같이 사용할 수 있다
스레드 x | 스레드 y |
lock(L) | |
lock(L) | |
LOAD D(0) to R1 | | |
R1 = R1(0) + 1 | | |
STORE R1(1) to D | 락이 풀릴 때 까지 기다림 |
unlock(L) | ↓ |
LOAD D(1) to R1 | |
R1 = R1(1) + 1 | |
STORE R1(2) to D | |
unlock(L) |
최종 결과가 기대했던 대로 D는 2가 됨을 확인할 수 있다
정리하면,
이처럼 락(lock)은
여러 스레드들이 같은 데이터를 동시에 사용할 때도
예상대로 동작할 수 있도록 동기화(synchronization) 시켜주기 위해 사용하게 된다
그래서 운영체제를 배울 때 락을 배우게 되는 것이다
(프로세스 사이에서도 shared memory 공간을 지정해서 데이터 공유를 하게 되면 같은 문제가 발생할 수 있고 락을 통해 해결할 수 있다)
쉬운코드는 기본에 진심입니다
👉 " 동기화를 하지 않으면 생기는 일 " 영상 보러 가기
👉 여러 락 종류를 알려주는 영상 보러 가기