본문 바로가기

OS

동시성

반응형

reference. 캄란 아미니 - 전문가를 위한 C

 

[동시성]

- 동시에 실행되는 프로그램 내부에 여러 로직이 있다는 의미로 동시성은 동시에 여러 작업을 처리할 수 있는 프로그램을 작성하는 강력한 도구이다. 동시성은 주로 커널에서 지원한다.

 

동시성을 논하면 같이 따라오는 것이 '병렬성'이다.

 

동시성 == 병렬성 으로 생각 할 수도 있는데, 이는 엄연히 다르다고 볼 수 있다.

 

[병렬성]

- 동시에 또는 병렬적으로 실행하는 두 작업이 있음을 의미로, 즉 두가지가 동시에 발생한다는 의미이다(동시 시스템은 이에 해당하지 않는다)

 

이정도로는 사실 뭐가 크게 다른지는 감이 오질않는다.

둘의 구분은 두 작업을 실행할 때 '일시 정지'가 발생하냐 안하냐로 받아들이면 이해하는데 도움이 된다.

 

예를 들어 책을 읽는 것으로 비유를 해보자면 (단순히 '읽는 행위'만으로 구분(책의 내용 공유는 고려안함))

 

책 1, 책 2 가 있을 때

 

- 동시성 -

 

내가 혼자서 두 책을 한 번에 읽는 행위와 같다.

혼자서 두 책을 읽으려 할 때는 불가피하게 한 책의 읽는 행위가 중단이 된다.

책 1을 다 읽고, 책 2를 읽거나, 책 1을 읽는 도중에 책 2를 읽거나, 가령 두 책을 한 줄 씩 읽더라도 양쪽 눈으로 각각의 책을 읽지 않는 이상 어느 한 쪽을 읽기 위해선 그 순간에는 다른 쪽은 읽을 수가 없다.

 

- 병렬성 -

 

나와 내 친구가 두 책을 한 번에 읽는 행위와 같다.

나와 내 친구가 각각 책 하나씩 읽으면, 내가 책을 읽는 동시에 친구도 책을 읽고 있기 때문에 읽는 행위가 중단될 일이 없다.

 

이처럼 동시성은 다중 작업(multitasking) 과 같은 개념으로, 시스템이 동시에 여러 작업을 관리할 때, 반드시 작업이 병렬적으로의 실행이 아니라는 것을 알아둬야한다.

작업 스케줄러(task scheduler) 가 있다면, 이 스케줄러에 의해 서로 다른 작업이 빠르게 전환해서, 짧은 시간동안 각각의 작업을 아주 조금씩 수행하는데, 이는 프로세스 유닛이 하나만 있을 때(단일 코어 일 때) 확실하게 두드러진다.

작업 스케줄러가 아주 빠르고 공평하다면, 작업이 전환된다는 것(context switching)을 알아차리지 못해 사용자 입장에선 병력적으로 실행되는 것처럼 보인다.

 

------------------------------------------------------------------------[ 여담 ]------------------------------------------------------------------------

 

멀틱스(Multics, Multiplexed Information and Computing Service) 운영체제는 다중 작업과 동시적인 프로세스를 처리하기 위해 고안된 최초의 운영체체였으며, 유닉스가 멀틱스 프로젝트로부터 얻은 아이디어를 토대로 만들어졌다.
위키백과에 따르면 당대에 멀틱스의 기술은 혁신적임에도 불구하고 상용 컴퓨터에 적용시키기엔 복잡하고 비용이 높아 평이 그리 좋지않았으나, 현대에 오면서 많은 운영체제가 멀틱스 운영체제에서 아이디어를 가져옮으로써 재평가 받고있다. 대표적으로 가상메모리, 보안, 시분할 시스템이 있다.

 

---------------------------------------------------------------------------------------------------------------------------------------------------------

 

거의 모든 OS 에서 다중 작업을 통해 동시적인 작업을 수행할 수 있으며, 특히 POSIX 호환 OS에서 잘 나타나는데 이는 POSIX 표준에시 기능을 명시하기 때문이다.


 

[작업 스케줄러 유닛]

- 모든 다중 작업(multitasking) OS 는 커널에 작업 스케줄러 유닛(task scheduler unit)이 있어야 한다. 스케줄러 유닛의 작동 방식과 역할은 다음과 같다.

 

1. 스케줄러는 실행을 기다리는 작업에 대한 대기열(queue)이 있다.

- 작업이란 실행 시 별도의 흐름에서 수행되어야 하는 작업들을 의미

 

2. 일반적으로 대기열은 우선순위가 높은 작업이 먼저 시작되도록 선택한다.

 

3. 모든 작업 사이에는 스케줄러가 프로세스 유닛을 관리하고 공유한다.

- 프로세서 유닛이 비어 있을 때(프로세스 유닛을 사용하는 작업이 없는 경우), 작업 스케줄러는 다른 작업이 프로세서 유닛을 사용하도록 하기 전에 대기열에서 이 작업을 선택해야한다. 작업이 종료되면 작업 스케줄러는 프로세서 유닛을 해제해 다시 사용할 수 있도록 한 뒤에 또 다른 작업을 선택한다. 이는 작업 스케줄링이라고 하며, 작업 스케줄러가 이를 단독으로 맡는다.

(cpu 코어(또는 스레드) 가 처리할 프로세스(또는 스레드)가 없을 때 스케줄러가 큐에서 다음 차례의 작업할 프로세스(또는 스레드)를 처리할 수 있게 연결해주며, 해당 작업이 끝나면 코어(또는 스레드)를 해제하고 다시 다음 차례의 작업을 연결해준다라고 해석해봅니다)

4. 작업 스케줄러가 사용할 수 있는 여러 스케줄링 알고리즘이 존재한다.

- 하지만 이 모든 알고리즘은 특정 요구 사항을 다뤄야한다. 예를 들면, 알고리즘은 모두 공평(fair)해야 하며 어떤 작업도 오랜 시간동안 선택되지 못한채 대기열에서 기아 상태(starved)로 계속 기다려서는 안된다.

 

5. 선택한 스케줄링 전략에 따라 스케줄러가 프로세스 유닛을 사용하려면 작업에 특정한 타임 슬라이스(time slice) 또는 타임 퀀텀(time quantum)을 지정해야 하거나, 스케줄러는 작업이 프로세서 유닛을 해제할 때까지 반드시 기다려야 한다.

 

타임 슬라이스 - OS 가 프로세스나 스레드에게 할당해준 CPU 점유 시간

 

6. 스케줄링 전략이 선점형(preemptive)일 떄, 스케줄러는 실행 중인 작업으로부터 CPU 코어를 강제로 회수헤서 다음 작업에 코어를 제공할 수 있어야한다. 이를 선점 스케줄링(preemptive scheduling)이라 한다.

또한 작업이 CPU를 자박적으로 해제하는 전략을 비선점 스케줄링(cooperative scheduling)리아 한다.

 

7. 선점 스케줄링 알고리즘타임 슬라이스를 서로 다른 작업 간에 균등하고 공평하게 배분하려고 한다. 우선순위가 있는 작업은 더 자주 선택될 수 있고, 스케줄러의 구현에 따라 더 긴 타임 슬라이스를 얻을 수 있다.

 

새 프로세스나 스레드가 생성되면 스케줄러 대기열에 새로운 작업으로 들어가 실행을 시작하기 전에 CPU 코어를 얻기 위해 대기한다. 시분할(time-sharing) 또는 선점형 스케줄러가 있을 때, 작업이 일정 시간 안에 로직을 완료할 수 없다면 작업 스케줄러가 CPU 코어를 강제로 회수한다. 그러면 작업은 다시 대기열로 들어가 다음 차례를 기다리며 해당 로직이 완료될 때까지 이 과정을 반복한다. 오늘날 대부분의 OS 는 선점형 스케줄러를 사용한다.

 

이 과정에서 문맥 교환이 발생하는데, 문맥 교환이 빠르면 빠를수록, 사용자는 작업이 더 병력적으로 실행된다고 여기게된다. 작업이 실행되는 동안 수많은 문맥 교환이 발생하는데 이 문맥 교환의 특징으론 예측이 불가능한 것이다. 언제 또는 어느 명령에서 문맥 교환이 발생할지 예측할 수 없다. 이러한 이유로 문맥 교환을 다룰 때 가장 좋은 방식은 '특정 명령어에 문맥 교환이 발생한 확률이 모든 명령어에 대해 같다'고 가정하는것이다. 즉, 모든 명령어는 실행할 때마다 문맥 교환이 발생한다는 내용으로 간단히 말하자면 인접한 두 명령어의 실행 간에도 틈이 있을 수 있다는 의미이다.

 

문맥 교환이 예측 불가능하지만, 동시적으로 실행되는 명령어에 대한 확실한 것이 있는데, 다음의 명령어 의사코드로 예시 설명을 이어보고자 한다.

작업 P
{
	num = 5
	num++
	num = num - 2
	x = 10
	num = num + x
}

 

위 명령어가 작업의 목적을 완수하려면 반드시 특정 순서로 실행되어야 한다는 의미이다. 기술 용어로는 모든 인접한 두 명령어 사이발생 전 제약(happens-before-constraint)이 있다고 표현한다.

num++ 명령어는 반드시 num = num - 2 명령어 이전에 발생해야 하는데 이 제약은 문맥 교환이 되더라도 반드시 지켜져야 한다.

 

다음과 같이 두가지 문맥 교환의 예로 들어보자

Run 1:
	num = 5
	num++
>>>> Context Switch <<<<
	num = num - 2
	x = 10
>>>> Context Switch <<<<
	num = num + x

 

Run 2:
	num = 5
>>>> Context Switch <<<<
	num++
	num = num - 2
>>>> Context Switch <<<<
	x = 10
>>>> Context Switch <<<<
	num = num + x

 

두 예시에서 처럼 문맥 교환의 발생 장소는 실행할 때마다 바뀔 수 있다. 하지만 위에서도 언급했던 발생 전 제약이 있기 때문에 이 제약으로 인해 특정 작업에 대해 전체적으로 결정론적인 행위가 존재하는 이유이다.

(특정 작업들이 모여 구성된 전체 작업이 일관되고 예측 가능한 결과를 낳는다는 것)

 

        결정론 :  과거의 원인이 미래의 결과가 되며, 이 세상의 모든 사건은 이미 정해진 곳에서 정해진 때에 이루어지게 되어 있었다는 이론

 

실행할 때마다 아무리 문맥 교환이 일어나더라도, 작업의 전체 상태(overall state) 는 동일하게 남아있다.

 

        overall state : 작업의 마지막 명령을 실행한 뒤 일련의 변수와 그에 해당하는 값을 의미

 

위의 예시 코드들에서 언제나 최종 상태가 존재할 것이고, 문맥 교환과 무관하게 14인 num 변수와 값이 10 인 변수 x 를 포함한다.

 

이 내용만으로 유추했을 때 동시성은 실행의 순서와 발생 전 제약 조건을 따라야 하므로 작업의 전체 상태에 영향을 줄 수 없을 것 같아 보이지만 '공유 자원'이 존재 할 때는 얘기가 달라진다. 공유 자원에 대해 읽기/쓰기 권한이 모두 있는 동시 작업 시스템에서 모든 작업이 공유된 변수를 '읽기'만 할 경우 위의 결과와 달라지지 않겠지만, '쓰기'를 하려는 순간 작업 스케줄러가 할당한 문맥 교환이 모든 작업의 전체 상태에 영향을 준다. 즉, 실행할 때마다 상태가 서로 다를 수 있다는 의미이다.

원치 않는 결과를 방지하려면 적절한 메커니즘을 사용해야 한다. 이는 역시 문맥 교환이 예측 불가능하고, 작업의 중간 상태(intermediate state)가 실행마다 다를 수 있기 때문이다. 

 

       intermediate state : 전체 상태(overall state)와는 반대로 어떤 명령어에 있으며 값을 갖는 일련의 변수

 

모든 작업은 완료 되었을 때 결정되는 단 하나의 전체 상태만 있지만, 특정 명령을 실행한 이후의 변수와 그 값에 해당하는 중간 상태는 매우 많다.

(여러 가지의 중간 상태를 통해 하나의 전체 상태가 도출되는데, 이 여러가지 중간 상태로 인해 전체 상태의 결과가 바뀔 수 있다는 의미)

 

문맥 교환의 영향을 상쇄하고, 다른 실행에서 동일하게 결정되는 결과를 얻으려면 적절한 동기화(synchronization) 방법을 사용해야 한다.

 

-----------------------------------------------------------------------[ 여 담 ]-----------------------------------------------------------------------

 

스케줄러의 작동 방식

 

만약 CPU 코어가 하나뿐이라면, 해당 CPU 코어를 사용하는 작업은 하나만 수행할 수 있다. 작업 스케줄러 역시 실행하려면 CPU 코어가 필요한 하나의 프로그램이다. 이 때, 다른 작업이 코어를 사용할 때는 CPU 코어를 사용하기 위해 다른 작업을 어떻게 처리하는지에 대해 의문이 생길 수 있다.

작업 스케줄러가 CPU 코어를 사용한다고 갖어했을 때, 먼저 작업 스케줄러는 타이머 인터럽트(timer interrupt)를 발생 시키기 위해 타이머를 맞추기 전에 대기열에서 작업을 선택한다. 그리고 CPU 코어를 떠나서 선택한 작업을 위한 자원을 할당한다. 참고로 작업 스케줄러는 각 작업에 특정 시간을 부여하고, 인터럽트가 작용할 시간이 존재하며, CPU 코어는 현재 작업의 실행을 중단해 작업 스케줄러가 CPU로 즉시 돌아오도록 한다고 가정되어 스케줄러는 이전 작업의 최신 상태를 저장새 대기열에서 다음 작업을 로드한다. 이 모든 과정은 커널이 다 차서 실행될 때까지 계속 실행된다.

물론 CPU 가 멀티 코어인 머신을 예외로, 멀티 코어에서 커널은 다른 코어에서 작업을 스케줄링 하는 동안 여러 코어를 사용한 수 있다.

 

---------------------------------------------------------------------------------------------------------------------------------------------------------


[동시성을 사용하는 일반적인 패턴]

- 사용하는 프로그래밍 언어와 상관없이, 프로그램이란 쉽게 말해 차례대로 실행되어야 하는 명령어의 모음이다. 즉, 주어진 명령어는 이전 명령어가 실행되기 전까지는 실행되지 않을 것이란 얘기로 순차척 실행(sequential execution)이라는 개념이다. 현재 명령어가 완료 되기까지 아무리 오래 걸리더라도, 다음 명령어는 현태 명령어가 완료될 때까지 반드시 기다려야 한다. 이를 두고 일반적으로 현재 명령어가 다음 명령어를 블로킹(blocking)한다고 한다. 때론 이러한 현재의 명령어를 블로킹 명령어(bloking instruction)라고도 한다.

 

첫 번째 패턴 - 무기한으로 실행 흐름을 차단할 수 있는 명령어가 있는 경우인데 이 지점에서 기존의 흐름을 두 개의 별도의 흐름 또는 작업으로 분할해야 한다. 이 후의 명령어를 실행해야 하는데 현재 명령어가 우선 완료될 때까지 기다릴 수 없을 때 이렇게 분할한다. 여기서 중요한 것은 '현재 명령어의 완료된 결과에 나중 명령어가 의존하지 않는다'고 가정하는 것이다.

 

다음의 예시 서버 설정과 의사 코드로 설명해보고자 한다.

 

- 클라이언트로부터 읽은 두 수의 합을 계산하고, 클라이언트에게 그 결과를 반환

- 클라이언트에 대한 서비스 여부에 상관없이, 서비스하는 클라이언트의 수를 파일에 규칙적으로 작성

- 한 번에 여러 클라이언트에게 서비스 할 수 있어야 함

 

계산기 서버
{
	작업 T1
	{
		N = 0
		서버준비
		영원히 수행
		{
			클라이언트 C를 기다리기
			N = N + 1
			C 에서 첫 번째 수를 읽고 X 에 저장
			C 에서 두 번째 수를 읽고 Y 에 저장
			Z = X + Y
			Z 를 C 에 쓰기(write)
			C 에 대한 연결을 종료
			N 을 파일에 쓰기(write)
		}
	}
}

 

ㄴ 첫 번째 명령어 [N = 0] 은 단순한 것이며 빠르게 완료됨

ㄴ 두 번째 명령어 [서버 준비] 는 합당산 시간에 종료될 것이 예측되므로 서버 프로그램의 실행을 블로킹하지 않음

ㄴ 세 번째 명령어 [영원히 수행] 은 메인 루프에서 막 시작하고 있으며, 루프 안으로 들어가는 대로 빠르게 종료되어야 함

ㄴ 네 번째 명령어 [클라이언트 C를 기다리기] 는 완료 시간을 알 수 없는 블로킹 명령어이다. 그러므로 5, 6 과 나머지 명령어들은 실행되지 않는다. 따라서 이들 명령어는 새 클라이언크가 올 때까지 반드시 대기해야 하고, 클라이언트가 온 뒤에 이들 명령어가 실행될 수 있다.

 

다섯 번째 명령어부터 열 번째 명령어는 반드시 새 클라이언트를 기다려야 하는데, 이는 이 명령어들이 네 번째 명령어의 출력에 의존적이며, 클라이언트가 허용되지 않으면 실행될 수 없다는 것이다.

 

하지만 열 한 번째 명령어 [N 을 파일에 쓰기(write)] 는 클라이언트의 유무에 상관없이 실행되어야 한다.

 

또한, 여섯, 일곱 번째 명령어는 모두 실행의 흐름을 막을 가능성이 있다. 이 명령어들은 클라이언트가 두 개의 수를 입력하기를 기다리는데, 결국 클라이언트에 달린 만큼 이 명령어가 언제 정확히 완료되는지는 예측할 수 없다. 즉, 프로그램이 실행을 계속하지 못하도록 하는 것이다. 

 

이 문제를 해결하려면 단일 작업을 서버 프로그램이 요구 사항을 만족할 수 있는 세 새의 동시 작업으로 분할해야 한다.

 

계산기 서버
{
	공유 변수 :  N
	작업 T1
	{
		N = 0
		서버 준비
		작업 T2 를 스폰
		영원히 수행
		{
			N을 파일에 쓰기
			30초 대기
		}
	}
	작업 T2
	{
		영원히 수행
		{
			클라이언트 C를 대기
			N = N + 1
			C에 대한 작업 T3 를 스폰
		}
	}
	작업 T3
	{
		C에서 첫 번째 수를 읽고 X에 저장
		C에서 두 번째 수를 읽고 Y에 저장
		Z = X + Y
		Z를 C에 쓰기(write)
		C에 대한 연결을 종료
	}

T1 작업

 

ㄴ 첫 번째 명령어 [N = 0] 초기화 실행

ㄴ 두 번째 명령어 [서버 준비]

ㄴ 세 번째 명령어 [작업 T2 를 스폰]으로 작업  T2 의 인스턴스(instance)를 생성하는 것으로 새 작업 생성 시간은 보통 빠르고 시간이 걸리지 않는다.

ㄴ 네 번째 명령어 [영원히 수행]

ㄴ 다섯, 여섯 번째 명령어 [N을 파일에 쓰기, 30초 대기]는 30초마다 공유된 변수 N 값을 파일에 계속해서 작성한다. 이는 '클라이언트에 대한 서비스 여부에 상관없이, 서비스하는 클라이언트의 수를 파일에 규칙적으로 작성' 이란 내용이 충족되어 작업 T1 은 다른 명령어에 어떠한 방해나 중단을 받지 않고, 프로그램이 종료될 때까지 N값을 파일에 규칙적으로 작성한다.

 

T2 작업

ㄴ 첫 번째 명령어 [영원히 수행]

ㄴ 두 번째 명령어[클라이언트 C를 대기] 는 새 클라이언트를 기다리므로 인해 다른 명령어를 블로킹하지만, 이는 T1 작업에서 무한 루프 내에서 이뤄지는 것이며, T2 작업의 명령어들에 한해서만 블로킹이 적용된다. 이는 T2 에서 인스턴스를 두 개 불러오면 인스턴스 중 하나에 있는 명령어를 블로킹하더라도 다른 인스턴스에 있는 명령어를 블로킹하지 않을 것이다.

ㄴ 세 번째 명령어 [N = N + 1]

ㄴ 네 번째 명령어 [C에 대한 작업 T3 를 스폰] T3 작업은 생성된 모든 클라이언트에 인스턴스로 생성될 수 있음

 

공유된 변수 N 에 대해 특히 T2 인스턴스와 같은 작업 중 하나가 그 값을 변경할 수 있다는 것을 주의해햐 한다. 한 작업이 수정할 수 있는 공유 변수가 존재하므로, 동시성 문제(데이터 경쟁(data race))가 발생하기 쉽다.

 

T3 작업

ㄴ 첫 번째 명령어 [C에서 첫 번째 수를 읽고 X에 저장] C 에서 값을 입력할 때까지 같은 인스턴스 내에서만 블로킹함

ㄴ 두 번째 명령어 [C에서 두 번째 수를 읽고 Y에 저장] C 에서 값을 입력할 때까지 같은 인스턴스 내에서만 블로킹함

ㄴ 세 번째 명령어 [Z = X + Y]

ㄴ 네 번째 명령어 [Z를 C에 쓰기(write)]

ㄴ 다섯 번째 명령어 [C에 대한 연결을 종료]

 

작업 T3만이 하는 일은 클라이언트와 통신해 입력된 수를 읽는 것이다. 그러고 나서 합계를 계산해 클라이언트에 다시 보낸다. 앞서 짚어본 대로 작업 T3 내의 블로킹 명령어는 다른 작업에 있는 명령어를 블로킹할 수 없다. 따라서 이 블로킹 행위는 T3의 동일 인스턴스에만 제한된다. T3의 특정 인스턴스에 있는 블로킹 명령어는 같은 작업 T3에 있더라도 다른 인스턴스에 있는 명령어를 블로킹 할 수 없다. 이러한 방식으로 서버 프로그램은 동시적인 방식으로 처음에 설정한 서버 내용을 충족시킬 수 있다.

 

위의 서버 종료 과정은 작업 T3 에 대한 특정 인스턴스는 블로킹 명령어 두 개를 전달한 뒤 클라이언트에 대한 연결을 끊거나, 작업 T2의 유일한 인스턴스가 완료될 때 종료된다.

 

또는 드물게 모든 읽기 작업이 완료되기까지 시간이 너무 많이 걸리고 들어오는 클라이언트의 수가 급증한다면, 작업 T3에서 너무 많은 인스턴스가 실행되고, 실행된 인스턴스는 각자의 클라이언트가 숫자를 입력할 때까지 모두 대기해야한다. 이 상황에허 상당한 양의 자원을 소모하게 되는데 시간이 지나 점점 더 많은 연결이 들어오면 서버 프로그램은 OS 에 의해 종료되거나 더 이상 클라이언트에게 서비스를 제공할 수 없게 된다.

(인스턴스가 계속 생겨남에도 불구하고, 인스턴스 처리가 되지않아 수많은 인스턴스 발생으로 인해 리소스가 부족해서 종료되거나, 더이상 실행이 되지 않는 상태가 되어버림)

 

이렇게 서비스를 중단하는 것을 서비스 거부(DoS: Denial of Service)라고 한다. 동시적인 작업을 하는 시스템은 시스템이 합당한 방식으로 클라이언트에게 서비스하지 못하도록 하는 이러한 극단적 상황을 극복할 방안을 설계해야한다.

 

------------------------------------------------------------------------[ 여담 ]------------------------------------------------------------------------

 

DoS 공격이 발생하면 서버에서는 자원 혼잡(congestion of resources)이 발생한다. 서버를 내려서 응답하지 않도록 하기 위한것인데, DoS 공격은 클라이언트가 서비스를 이용할 수 없도록 특정 서비스를 차단하려는 네트워크 공격에 속한다.

서비스 중단을 의도한 익스플로잇(exploit, 취약점 공격)을 포함한 광범위한 공격이 네트워크 공격에 포함된다. 또한 여기에는 네트워크 인프라를 중단시키려는 네트워크 플러딩(flooding)도 포함될 수 있다.

 

---------------------------------------------------------------------------------------------------------------------------------------------------------

 

이렇게 종료 시각을 알 수 없거나 완료할 때까지 시간이 오래 걸리는 블로킹 작업이 있을 때마다 작업을 두 개의 동시 작업으로 나누어야 한다.

 

두 번째 패턴 - 하나 또는 여러 명령어가 완료하는 데 시간이 너무 많이 걸릴 경우, 이들을 별도의 작업에 넣고 새 작업을 메인 작업과 동시에 실행할 수 있다. 이는 첫 번째 패턴과는 다르다.

비록 정확하지는 않더라도 완료 시간을 알고 있으나, 빨리 끝나지는 않는 경우이기 때문이다.


[공유 상태]

- 상태특정 시점의 일련의 변수와 그에 해당하는 값을 떠올린다.

작업의 전체 상태 -  작업의 마지막 명령어가 실행되었을 때 기존의 모든 공유되지 않은 변수와 그에 해당하는 값의 집한 상태라고 지칭한다.

 

작업의 중간 상태 - 비슷하게 작업이 특정 명령을 실행했을 때 기존의 모든 공유되지 않은 변수 및 그에 해당하는 값의 집합이다. 따라서 명령어마다 중간 상태가 있으며, 중간 상태의 수는 명령어의 수와 일치한다. 

 

마지막 중간 상태 - 작업의 전체 상태와 같다.

 

공유 상태특정 시점에 동시 작업 시스템이 읽거나 수정할 수 있는 변수와 그에 해당하는 값의 집합니다.

공유 상태는 작업이 소유하지 않으며 작업에 국한되지 않는다. 그리고 공유 상태는 시스템에서 실행 중인 모든 작업이 언제든 읽거나 수정할 수 있다. 그 중에도 수정 가능한 공유 상태는 세심하게 보호하지 않으면 심각한 문제를 일으킬 수 있다(읽기 전용인 공유 상태크게 문제 없음

 

다음의 동시 작업 두 개로 구성된 시스템 예시 의사 코드로 설명해보고자 한다.

동시시스템
{
	공유 상태
	{
		X : 정수 = 0
	}
	작업 P
	{
		A : 정수
		1.	A = X
		2.	A = A + 1
		3.	X = A
		4.	X 를 출력
	}
	작업 Q
	{
		B : 정수
		1.	B = X
		2.	B = B + 2
		3.	X = B
		4.	X를 출력
	}
}

 

이 시스템에서 작업 P와 Q가 동시에 실행되지 않는다고 가정할 때, 이 작업들은 순차적으로 실행된다. P의 령령어가 Q모다 먼저 실행된다고 할 때 개별 작업의 전체 상태와는 상관없이 전체 시스템의 전체 상태는 3이라는 값을 갖는 공유 변수 X가 될 것이다.

 

시스템을 역순으로 실행해 Q의 첫 번째 명령어 다음에 P의 명령어가 실행될 때는 동일한 전체 상태를 얻긴 하지만 이는 일반적인 사레는 아니다. 대개는 두 개의 다른 작업을 역순으로 실행하면 서로 다른 상태가 될 수 있다.

 

즉, 순차적으로 작업을 실행하면 문맥 교환을 걱정하지 않고도 결정론적인 결과가 나온다.

 

이제 위의 작업들을 CPU 코어에서 동시에 실행된다고 가정해서 여러 명령어에서 발생하는 여러 문맥 교환을 고려하면 P와 Q의 명령어를 실행하는 시나리오는 다수 존재하는데 다음과 같은 시나리오를 예를 들 수 있다.

 

작업 P 작업 스케줄러 작업 Q
  문맥 교환  
    B = X
    B = B + 2
  문맥 교환  
A = X    
  문맥 교환  
    X = B
  문맥 교환  
A = A + 1    
X = A    
  문맥 교환  
    print X
  문맥 교환  
print X    
  문맥 교환  

 

위의 시나리오는 특정 지점에서 문맥 교환이 발생하는 여러 시나리오 중 하나이며, 이 같은 시나리오는 인터리빙( Interleaving) 이라고 한다. 동시 작업 시스템에서 문맥 교환이 발생할 수 있는 여러 지점에 따라 다수의 인터리빙이 존재할 수 있고, 실행할 때마다 이어란 인터리빙들 중 오직 하나만 발생하기 때문에 인터리빙은 예측할 수 없다.

 

위의 인터리빙에서 첫 번째와 마지막 열에 보이듯, 명령어 순서와 발생 전 제약은 보존된다. 하지만 실행 사이에는 틈이 존재하고, 이 틈은 예측할 수 없으며, 실행을 추적하면 최종 결과로 둘 다 3을 출력할 것으로 예상을 했지만, P 작업은 1을 출력하고, Q 작업은 2를 출력하는 결과를 보여준다.

(의사 코드에서 전체 상태는 3 이었고, 이 전체 상태의 측면에서 본다면 인터리빙의 예상 결과값 또한 전체 상태와 같아야 되니  X 는 마지막에 3으로 예측되어 둘 다 3이 출력 될것이라고 예상)

 

위의 '전체 상태가 3 이어야 한다" 같은 제약은 프로그램에서 출력해서 보여주는 것과 별개일 수 있다. 게다가 예측할 수 없는 문맥 전환에 직면했을 때, 변하지 않은 채 남아 있어야 하는 다른 중요한 제약이 존재한다. 이러한 제약들은 데이터 경쟁 또는 경쟁 상태(race condition) 를 포함하지 않을 수 있으며, 메모리 누수가 전혀 없거나 충돌이 발생하지 않는다.

(제약들로 인해 데이터 경쟁이나 경쟁 상태가 일어나지 않은 수 있으며, 제약들이 적용되므로 인해 메모리 누수나 충돌이 자연스럽게 회피된다)

이 모든 제약은 프로그램의 출력보다 훨씬 중요하다. 실제 여러 응용프로그램에서 프로그램은 아무것도 출력하지 않는다.

 

다음의 다른 인터리빙을 예로 들어보자

작업 P 작업 스케줄러 작업 Q
  문맥 교환  
    B = X
    B = B + 2
    X = B
  문맥 교환  
A = X    
A = A + 1    
  문맥 교환  
    print X
  문맥 교환  
X = A    
print X    
  문맥 교환  

 

위의 인터리빙에서 작업 P는 3을 출력하지만 Q는 2를 출력한다. 작업 P가 세 번째 문맥 교환 이전에 공유된 변수 X 값을 업데이트함으로 인해 작업 Q는 당시 값인 2 였던 X 를 출력하게된다. 이러한 경우를 변수 X에 대한 데이터 경쟁이라 한다.

 

또한, A++ (A = A + 1) 같은 간단한 구문 일지라도 이 구문이 단일 타임 슬라이스에서 실행되지 않는다는 점을 나타낸다. 즉, 이는 원자적인 작업이 아니며 3개의 더 작은 원자적 명령어로 구성되어있다는 것이다. 원자적 명령어는 더 작은 작업으로 나뉠 수 없으며 문맥 교환으로 인해 인터럽트될 수도 없다.
(A++ 구문에서 조차 A를 읽기, 1 증가, A에 쓰기 라는 세 동작이 원자적 명령이 3개가 실행되고, 이 원자적 명령 자체가 실행 중일 때는 인터럽트에 영향을 받지 않지만, 명령어 사이에선 충분히 문맥 교환이 일어날 수 있어 값이 어떻게 바뀔지 모른다는 것)

 

이 예제를 통해 단 두 개의 동시 작업 사이에 공유 상태가 하나뿐이더라도 전체적인 결과는 '덜 결정론적'이라는 것을 확인했다. 다시 말해 결과라 확정되니 않는 것에 관한 문제를 살펴본 것이다.

 

이처럼 공유 자원에 접근하려는 작업이 이보다 더 많은 케이스가 있기 때문에 동시성 문제는 깊게 공부래 결과를 예측할 수 있는(결정론적인) 메커니즘을 찾아야 한다.

 

 

반응형

'OS' 카테고리의 다른 글

동기화3  (6) 2024.09.12
동기화2  (14) 2024.09.09
프로세스 동기화  (0) 2024.08.22
스레드  (0) 2024.08.20
프로세스  (0) 2024.08.16