글 작성자: Key Ryung

Concurrency Condition[0](feat. side effect) - 상태를 가진다는 것

Concurrency Condition[1](feat. thread, process) - 컴퓨터가 바라보는 상태란?

 

Introduction

지난 포스팅에서 상태를 가지므로써 Side Effect가 생길 수 있고 그에 따라 Race Condition이 발생하는 부분을 살펴보았다. 이번 포스팅에서는 컴퓨터가 우리가 말하는 "상태"를 어떻게 다루길래 이런 문제가 생기는가에 대해서 컴퓨터 구조와 OS 관점에서 살펴보려고 한다.

 

CPU always work!

게임하는 도중에 CPU가 지금 어떤 연산을 하고 있는지, 크롬으로 쇼핑몰에 들어가 쇼핑을 할 때 CPU가 어떤 일을 수행하고 있는지 모르는 것처럼 우리가 평상시 컴퓨터를 사용할 때에는 CPU가 무엇을 하는지 1도 관심이 없다. 하지만 반대로 CPU는 우리(User Program)가 무엇을 하는지 관심이 많다(처리를 해준다).

컴퓨터 구조
OS Privilege Level Ring

컴퓨터가 발전해나가며 OS라는 프로그램이 개발되었고 현재 PC에서는 필수적으로 설치한다. 발전과정에서 하드웨어(CPU, 메모리, SSD), OS(Window, Linux, Mac), 응용 프로그램(Intellij, Chrome)까지의 계층을 만들어 각각의 영역을 분리시키고 수행하는 일이 다르다. 이 구조에서 안쪽에서 바깥쪽 Ring을 볼 수 있지만 바깥쪽 Ring 에서는 안쪽 Ring이 무엇을 수행하는지 알 수 없게 만들어 보안 관련 문제를 해결하였다. 따라서 내가 만든 프로그램(User Program)은 CPU를 제어하거나 상태를 직접적으로 알지 못한다.

 

왜 Race Condition을 말하는데 OS나 CPU 얘기가 나와야 되는지 알기 위해서는 용어의 정리가 필요하다. 처음 Race Condition이 왜 발생하는지 알아볼 때 이런 부분에 대한 개념이 확실하게 안잡혀있다 보니 이해하는 데 힘들었다.

 

Program, Process, Thread

이번 포스트를 작성할 때 개념에 대한 다양한 글들을 조사해보았는데 생각보다 정의에 대한 관점이 다르기도 하고 잘 못 설명하는 부분도 많았다. 특히 Race Condition을 컴퓨터 관점에서 이해할 때 꼭 필요한 Program, Process, Thread와 관련해서도 잘못된 개념이 많아 정리하고 넘어가려고 한다.

 

프로그램 : 저장 장치에 저장된 명령문의 집합체(어플리케이션, 앱), 프로그램은 컴퓨터 관점에서 하드 디스크만 사용하는 수동적인 존재다.

프로세스 : 실행중인 프로그램으로, 저장 장치에 저장된 프로그램이 메모리에 올라갔을 때를 말하고, 운영체제의 CPU 스케쥴링 알고리즘에 따라서 CPU도 사용하고 필요에 따라 입력과 출력을 하는 능동적인 존재다.

프로그램, 프로세스, 스레드, CPU까지

따라서 설명할 때 "프로세스는 프로그램이다"라고 설명한다면 반만 설명되었다고 볼 수 있다. 그리고 이 부분을 잘 구분해야 현대 OS에서의 프로세스, 스레드 개념에 대해서 이해하기가 편하다.

 

Process

프로세스가 만들어지(프로그램이 메모리에 올라가면)면 운영체제는 해당 프로세스의 정보를 가지고 있는 PCB(Process Control Block)을 만들고 저장한다. 프로세스 생성 과정을 정리하면

1. .exe 파일을 클릭하여 실행

2. 운영체제는 해당 프로그램의 코드영역과 데이터 영역을 메모리에 로드하고 빈 스택과 빈 힙을 만들어 공간을 확보

3. 이제 이 프로세스를 관리하기 위한 PCB를 만들어서 값을 초기화 한다.

 

Thread

한참 먼 과거에는 운영체제가 작업을 처리하는 단위를 "프로세스"로 정의했었다. 그렇게 멀티 프로세스를 통한 프로그램을 만들어 잘 활용하였다. 근데 여기서 문제가 있었다. 새로 생성된 프로세스들은 독립적이기 때문에 통신하려면 IPC(Inter Process Communication)을 이용하여 통신하여 비용이 비쌌다.

이런 부분에서 좀 더 CPU를 효율적으로 사용하기 위해 Thread라는 개념을 도입한다. 한 Process내에 스레드는 1개 이상 존재한다.

헷갈리지 말자! Hardware Thread, Software Thread
1 Core - 2 Thread
8 Core - 32 Thread
와 같은 곳에서 말하는 Thread는 CPU 설계 관점에서 보는 Hardware 스레드이다. 관련된 내용은 추가적으로 포스트를 올릴 생각이다.
관련된 내용을 찾아보고 싶다면 Hyper Threading/SMT 키워드에서 검색하면 나온다.

하드웨어적 관점에서 Thread는 CPU를 논리적으로 구분하는 단위이다.

 

현재 운영체제가 CPU를 통해 처리하는 작업의 단위는 프로세스 내에 스레드, 프로세스는 자원의 단위로 말할 수 있다.

그렇다면 위에서 얘기한 프로세스와 비교해보면 Thread는 프로세스 내에서 여러 개 존재할 수 있고 프로세스내에서 자원을 공유할 수 있다.(작업의 단위이므로)

실제 프로그램으로 예를 들면, 크롬의 경우 하나의 탭에 한 개의 프로세스가 실행된다. 100개의 탭을 열면 100개의 프로세스가 생성된다. 하지만 파이어 폭스의 경우 처음 4개의 탭에서만 프로세스가 생성, 추가되고 그 이상의 탭에서는 프로세스 내에 스레드를 추가하는 방식으로 동작한다.

 

정리하면 Thread는 일반적으로 운영체제의 일부인 스케쥴러에 의해 독립적으로 관리될 수 있는 프로그래밍된 명령어의 가장 작은 Sequence라고 말할 수 있다.

 

그래서 Race Condition이 왜 일어날까?

앞서 말했듯 Thread는 자원을 공유한다. 프로세스를 생성할 때 빈 힙과 빈 스택을 생성한다고 했다. 스레드는 각각의 스택영역을 따로 지정하여 분리되어있지만 힙 영역(Heap area)은 공유한다.

스레드 저장 구조

앞서 포스팅했던 Race Condition[0](feat. side effect) - 상태를 가진다는 것에서 만들었던 nstance는 힙영역에 저장된다. 이제 Race Condition을 설명할 조건이 모두 충족되었다.

공유되는 자원
여러개의 스레드가 공유되는 자원에 동시에 접근

공유되는 자원에 여러 개의 스레드가 접근하는 것이 바로 Race Condition! 동시성 문제! Thread-safety를 망치는 원인!

1. 상태를 갖는다는 것은 공유되는 자원(힙 영역)을 가질 수 있는 것이고

2. Side-Effect로 인해 여러 스레드가 공유되는 자원에 대해 접근하여 수정하게 되면 Race-Condition이 발생한다.

 

Multi-Process vs Multi-Thread Program

앞서 언급했듯, 프로세스는 서로 독립적이기 때문에 "멀티 프로세스 프로그램(각 프로세스마다 싱글 스레드)"을 만든다면, 이런 문제가 생기지 않을 것이다. 하지만 프로세스끼리 정보를 주고 받기 위해서는 IPC라는 상대적으로 비싼 방식을 사용해야 하는 단점이 있다. 반대로 "멀티 스레드 프로그램"에서는 공유하는 자원이 있기 때문에 이런 통신 비용이 저렴해지고 추후 다룰 Context-Switching 측면에서도 이득을 볼 수 있다. 하지만 공유되는 자원과 여러 스레드가 접근하는 환경이 이루어지기 때문에 동시성 문제(Race-Condition 등등)로 Thread-Safety를 고민해야 된다.

각각의 장단점이 명확하기에 상황에 따라 적절히 선택하는 것이 가장 올바른 방법으로 보인다.

 

그래서 어떻게 Race-condition을 해결하는가?

앞선 포스트부터 얘기했지만 단점은 없애고 장점은 살리는 프로그램을 만들기 위해서 많은 분들이 노력하여 언어적인 측면에서의 해결방법, OS 관점에서의 해결방법, 개발론적인 부분에서의 해결방법 등 많은 시도들이 이루어졌다. 이런 부분에 대해서는 추후 포스트에서 살펴볼 것이다.