동시성은 한 번에 여러 일을 처리하는 것이다. 병렬성은 한 번에 여러 일을 하는 것이다.
... 동시성은 (병렬화가 꼭 필요하지는 않지만) 병렬화할 수 있는 문제를 해결하는 데 필요한 구조를 제공한다.
- 롭 파이크 (Go 언어 공동 창시자)
동시성에 대해 본격적으로 이야기하기 앞서 몇 가지 용어와 개념을 정리해보겠습니다.
동시성(Concurrency)과 병렬성(Parallelism) 용어에 대해선 의견이 다양하다고 합니다.
저는 병렬성은 물리적으로 병렬로 동시에 실행하는 것이고, 동시성은 time sharing을 통해 동시에 실행하는 것처럼 보이는 것으로 이해하고 있었습니다. 대부분 비슷하게 설명하고 있는 것 같고 이게 롭 파이크의 해석과도 크게 달라보이지는 않습니다. 다만 롭 파이크의 견해대로라면 동시성은 병렬성을 포함하는 상위 집합이 되겠습니다.
일반적인 운용 환경의 랩톱에서 4코어인 CPU는 200개 이상의 프로세스를 일상적으로 실행하는데, 이를 '병렬'로 처리하려면 200개의 코어가 필요합니다.
CPU는 한번에 4가지가 넘는 작업을 할 수 없지만, OS의 프로세스 관리하에 수백 개가 실행할 수 있는 동시성을 제공합니다. 하지만 이 경우에 (200개 만큼의)병렬은 아닌 것이죠.
멀티 프로그래밍과 멀티 프로세싱은 얼핏 같은 의미인 것 같지만 다릅니다. (사실 요즘에서는 멀티 프로그래밍이란 말 자체가 어색하긴 합니다만)
공룡책에 멀티 프로그래밍이란 여러 프로그램을 메모리에 올릴 수 있는 구조를 의미합니다.
PC에서 한번에 only 하나의 프로세스만 실행되는 과거를 떠올려봅시다. 이때는 노래를 들으면서 인터넷으로 검색하며 워드 작업을 하는 일을 할 수 없었습니다.
만약 어떤 파일을 다운로드 받고 있다면 다운로드가 끝나는 동안 컴퓨터는 먹통이 됩니다.
멀티 프로그래밍의 목적은 여러 프로세스가 실행되게 함으로서 CPU 효율의 극대화하기 위한 목적이었습니다. 다운로드 같은 장시간 I/O 작업이 생기는 동안에 CPU는 다른 프로세스를 실행함으로써 항상 CPU가 일하게 합니다.
메인 메모리에 모든 프로세스를 올리기는 버겁기 때문에, 디스크에 job pool을 만들고 메인 메모리에 할당되기를 기다립니다. 그리고 메인 메모리에 올라간 프로세스들이 실제로 실행되는 것이죠.
타임 쉐어링은 프로세스마다 부여된 CPU 타임만큼 실행되고 context switch 합니다. 타임 쉐어링이 없다면 메인 메모리에 여러 프로세스가 있다 할지라도 interrupt이나 system call이 있기 전에는 다른 프로세스가 CPU를 점유할 수 없지만, 타임 쉐어링은 시간만 기다리면 (공평하게) 차례가 돌아옵니다.
process scheduling 설명하며 이런 개념들을 설명하고 있습니다.
main-memory 안에서 ready 상태의 프로세스를 execute하는 스케줄링을 short-term scheduling,
job pool -> main memory 로 프로세스를 할당하는 스케줄링을 long-term scheduling으로 구분짓기도 하네요. 이때 CPU-bound process와 I/O-bound process를 적절히 섞어주는 것이(good process mix) 중요하다고 하네요.
요약하자면 멀티 프로그래밍과 타임 쉐어링은 동시성을 실현하는 구조적 토대입니다.
우리가 말하는 멀티태스킹은 동시성을 의미하며, 멀티 프로세싱, 멀티 스레딩 그리고 비동기 처리는 그 수단에 해당합니다.
코드를 동시에 실행하는 객체로 프로세스, 스레드, 코루틴이 있습니다. 각각은 독립적인 상태와 콜 스택을 갖습니다.
프로세스
실행 중인 프로그램의 인스턴스로 메모리와 CPU 타임 슬라이스를 사용하여 동작합니다. 프로세스는 각자의 메모리 공간을 갖고 격리됩니다. 프로세스간에는 shared memory나 message passing(소켓, 파이프, RPC, 메세지 큐) 등의 방식으로 통신합니다. (IPC, Inter Process Communication)
스레드
프로세스 안에 있는 실행 유닛으로 CPU 사용의 기본 유닛입니다. 프로세스가 시작되는 것은 메인 스레드를 시작하는 것으로 시작되며 시스템 콜을 통해서 여러 스레드를 생성할 수 있습니다. 각 스레드는 ID, PC, register값, 스택 같은 실행 상태를 가지며(TCB, Thread Control Block) 프로세스로부터 data, code, heap section 그리고 open files 등 OS 자원을 공유합니다.
코루틴
OS의 관리(context swiching, scheduling 같은)를 받는 process, thread와는 달리 코루틴은 코드 레벨에서 관리되는 object입니다. 코드를 실행하는 스레드는 수백 수천개의 코루틴을 가질 수 있습니다. OS 컨텍스트 스위치가 없기 때문에 여기에서 발생하는 overhead를 최소화하며 코드 레벨의 instance들을 공유합니다. (일반적으로 하나의 스레드에서 동작하는데, multi threaded program에서는 반드시 한 스레드에 종속되는 것은 아니라고 하네요.)
코루틴은 cooperative(non-preemptive) multitasking을 지원합니다. OS의 스케줄링이 없는 대신 프로그래머가 suspend, await, yield 등을 통해서 적절히 스케줄링해야 합니다.
코루틴은 비동기 처리를 쉽게 만들어주어 비동기 처리를 위해서도 많이 쓰입니다. (코루틴을 설명하며 비동기를 설명하고 비동기를 설명하며 코루틴을 설명하는 텍스트들이 많아서 코루틴 is equal to 비동기라고 착각할 수 있는데 엄연히 따지면 둘은 다릅니다! 제가 그랬습니다...이걸 받아들이기까지 꽤 혼란스러웠습니다.ㅜㅜ)
thread를 lightweight process 라고 하기도 하고,
coroutine을 lightweight thread 라고 하기도 합니다.
스레드는 프로세스에 비해
1. Responsiveness
웹 서버를 떠올리면 알 수 있듯이 멀티 스레딩 시스템에서 한 스레드가 blocked 되거나 오랜 연산 작업 중이더라도 다른 스레드가 계속 서비스를 할 수 있습니다.
2. Resource Sharing
IPC는 비싼 연산이고 까다롭습니다. 이에 비해 스레드는 기본적으로 프로세스의 공유 메모리나 자원에 쉽게 접근할 수 있습니다. 프로세스의 경우도 마찬가지이지만 공유 영역(critical section)을 접근하는 경우 synchronization issue는 주의해야겠지만요.
3. Economy
프로세스는 스레드에 비해 무겁습니다. 각자 고유한 메모리를 공간을 차지하며, context switch될 때마다 PCB 저장 및 복원, CPU 캐시 메모리(메인 메모리와 CPU 사이에서 데이터 캐싱 역할)를 초기화 해야합니다.
스레드도 역시 context switch를 할 때 TCB 저장 및 복원 과정이 있으나, 프로세스에서 공유되는 정보를 제외한 스레드 고유의 register, stack 정보만 저장하기 때문에 빨리 읽고 쓸 수 있습니다. 그리고 스레드 context switch가 일어나더라도 CPU 캐시 메모리는 초기화되지 않습니다. (단, 다른 코어에서 실행되는 경우에 해당 캐시 메모리에 스레드 컨텍스트 정보가 로드되어야 하기 때문에 초기화될 수 있다고 합니다.)