reference. 캄란 아미니 - 전문가를 위한 C
동시성 또는 다중 작업은 OS 의 커널에서 제공하는 기능이다. 모든 커널이 처음부터 동시적인 것은 아니었지만, 오늘날 대부분은 동시성을 지원한다. UNIX 의 첫 번째 버전은 동시적이지 않았지만 탄생 직후에 이 기능을 갖추게 되었다.
POSIX 호환 OS의 동시성은 일반적으로 두 가지 방식으로 제공된다. 동시 프로그램은 멀티 프로세싱(multiprocessing)이라고 하는 서로 다른 프로세스로 실행하거나 멀티스레딩(multithreading) 이라고 하는 동일한 프로세스에 있는 서로 다른 스레드로 실행할 수 있다.
[동시성을 지원하는 커널]
- 오늘날 개발되고 유지되는 거의 모든 커널은 다중 작업을 한다. 모든 커널은 실행 중인 여러 프로세스 및 스레드 사이에서 CPU 코어를 공유하는 작업 스케줄러 유닛(task scheduler unit)이 있다.
프로그램을 실행할 때마다 새 프로세스가 생성되고, 프로그램 로직은 해당 프로세스 내부에서 실행된다. 프로세스는 서로 떨어져 있으며, 마치 메모리처럼 한 프로세스는 다른 프로세스로 접근할 수 없다. 스레드는 프로세스와 매우 유사하지만, 특정 프로세스에 국한한다. 스레드는 동시적인 방식으로 여러 명령어를 함께 실행하는 여러 실행 스레드를 이용해 단일 프로세스에 동시성을 도립한다. 단일 스레드는 두 프로세스 간에 공유괼 수 없으며, 스레드를 소유한 프로세스에 국한되어 바인딩된다.
한 프로세스의 모든 스페드는 공유 자원으로써 자신들을 소유한 프로세스의 메모리에 접근할 수 있다. 하지만 모든 스레드에는 같은 주소에 있는 다른 스레드가 접근할 수 있는 스택 영역이 존재한다. 게다가 프로세스와 스레드는 모두 CPU를 공유할 수 있으며, 대부분의 커널에 작업 스케줄러는 이들 사이에 CPU 코어를 공유하는 같은 스케줄링 알고리즘을 사용한다.
참고로 커널 수준에서 논할 때는 스레드나 프로세스라는 용어 대신 작업(task) 라는 용어를 더 선호한다. 커널 관점에서 보면 명령어를 실행하기 위해 CPU 코어 획득을 대기하는 작업의 대기열이 있으며, 작업 스케줄러 유닛은 공정한 방식으로 모든 작업에 이 기능을 제공할 의무가 있다.
------------------------------------------------------------------------[ 여담 ]------------------------------------------------------------------------
UNIX 계열 커널에서는 일반적으로 프로세스 및 스레드에 대해 둘 다 작업(task)라는 용어를 사용한다. 사실 스레드나 프로세스는 사용자 공간(user space) 용어이며 커널 용어로는 사용할 수 없다. 따라서 UNIX 계열 커널에는 여러 작업 사이에서 CPU 코어에 대한 접근을 공정하게 관리하는 작업 스케줄러 유닛이 있다.
---------------------------------------------------------------------------------------------------------------------------------------------------------
다른 커널에서는 작업 스케줄러가 스케줄링하기 위해 다른 전략 및 알고리즘을 사용한다. 하지만 대부분의 스케줄링 알고리즘은 크게 다음 두 가지로 분류할 수 있다.
- 비선점 스케줄링
작업에 CPU 코어를 부여하고 협력 작업이 CPU 코어를 해제하기를 대가하는 것이다. 이 접근 방식은 대부분 일반적인 경우, 작업에서 CPU 를 회수하는 강제력이 없다는 점에서 선점적(preemptive)이지 않다. 스케줄러가 선점을 통해 CPU르 회수하려면 우선순위가 눞은 선점 신호(preemptive singal)가 있어야 한다. 이 신호가 없다면, 시스템의 스케줄러와 모든 작업은 진행 중인 작업이 CPU 코어를 마음대로 해제할 때까지 기다려야 한다.
오늘날의 커널은 보통 이렇게 설계되지 않지만, 실시간 프로세싱(real-time processing)과 같은 아주 특정한 응용 프로그램에 대해 비선점 스케줄링을 사용하는 커널은 여전히 찾아볼 수 있다. macOS 와 WINDOW 초기 버전은 비선점 스케줄링을 사용했지만, 최근에는 선점 스케줄링 방식을 사용한다.
- 선점 스케줄링
스케줄러가 CPU 코어를 회수할 때까지 CPU 코어를 사용할 수 있다. 특정 유형의 선점 스케줄링에서는 작업이 특정 시간동안 주어진 CPU 코어를 사용할 수 있다. 이러한 유형의 선점 스케줄링은 시분할이라고 하며, 현재 커널에서 가장 많이 사용되는 스케줄링 전략이다. CPU 를 상요하기 위해 작업에 주어지는 시간 간격에는 다양한 이름이 있는데, 여러 학술 문헌에서 타임 슬라이스, 타임 슬롯(time slot) 또는 양자화(quantum)라고 한다.
또한 사용한 알고리즘에 따라 다양한 시분할 스케줄링이 있다. 라운드 로빈(RR, round-robin)은 시분할 알고리즘에서 가장 폭넓게 쓰이며 일부 수정을 거쳐 여러 커널이 사용한다. 라운드 로빈 알고리즘은 이번 경우 CPU 코어에 해당하는 공유 자원에 대해 공정하고 기아 상태가 없는(starvation-free) 접근을 허용한다.
라운드 로빈 알고리즘은 간단하면서도 우선순위를 지정하지 않지만, 한편으론 작업에 대해 여러 우선순위 수준을 두도록 수정할 수 있다. 우선순쉬 수준을 다르게 두는 것은 현대적인 커널의 필요 조건이다. 커널 자체 또는 커널 내의 다른 중요 유닛에 의해 시작되는 특정 유형의 작업이 있는데, 이 작업은 다른 일반적인 작업보다 먼저 실행되어야 하기 때문이다.
S/W 동시성을 도입하는 데는 두 가지 방식이 있다.
- 멀티프로세싱
다중 작업 환경에서 병렬 작업을 하도록 사용자 프로세스를 사용한다.
- 멀티스레딩
단일 프로세스 내에서 작업을 병렬적인 실행의 흐름으로 분할하기 위해 사용자 스레드를 사용한다.
큰 S/W 프로젝트에서는 두 가지 기법을 결합해서 사용하는 일도 매우 흔하다. 두 기법 모두 S/W 에 동시성을 도입하지만, 서로 다른 속성과 관련된 근본적인 차이가 있다.
[멀티프로세싱]
- 동시 작업을 하는 프로세스를 의미한다. 웹 서버의 공용 게이트웨이 인터페이스(CGI, common gateway inteface) 가 대표적인 예이다. 이 기술을 사용하는 웹 서버는 각 HTTP 요청에 대해 새로운 인터프리터 프로세스(interpreter process)를 시작한다. 이러한 방식으로 웹 서버는 동시에 여러 요청을 처리할 수 있다.
이러한 웹 서버에서는 많은 요청을 처리하기 위해 여러 개의 인터프리터 프로세를 동시에 스폰하고 실행한다. 이들은 서로 다른 프로세스이므로 격리되어 있고 서로에 대한 메모리 영역을 볼 수 없다. 운 좋게도, CGI 사례에서 인터프리터 프로세스는 서로 통신하거나 데이터를 공유할 필요가 없다. 그렇다고 항상 그렇지는 않다.
여러 프로세스가 동시게 여러 작업을 하는 많은 사례가 존재하며, 프로세스들은 S/W가 계속 가능하도록 필수 정보를 공유해야 한다. 하둡(hadoop) 인프라를 예로 들수 있는데 하둡 클러스터에는 여러 노드가 있고, 각 노드에는 클러스터를 게속 실행하는 프로세스가 여러 개 있다.
이러한 프로세스가 클러스터를 계속 실행하려면 지속해서 정보를 공유해야 한다. 이렇게 노드가 여러 개인 분산 시스템의 예는 더 많다(글러스터 gluster, 카프카 kafka, 암호화폐 네트워크 등). 이들 모두 계속 작동하고 실행하려면, 서로 다른 노드에 있는 프로세스 사이에서 상당한 분량의 통신 및 메시지를 전달해야 한다.
프로세스나 스레드가 중간에 공유 상태 없이 작동하는 한, 멀티프로세싱과 멀티스레딩 간에 그리 큰 차이는 없다. 스레드 대신 프로세스를 사용할 수 있으며 그 반대도 가능하다. 하지만 스레드와 프로세스 사이에 공유 상태를 도입한다면, 이들을 사용하거나 조합하는 것 사이에 큰 차이를 확인할 수 있다. 한 가지 차이점은 사용 가능한 동기화 기술이다. 이러한 메커니즘이 사용하도록 제공하는 API 는 겅의 같지만, 멀티프로세스 환경에서 작동하는 것은 훨씬 더 복잡하며 기본 구현도 다르다. 멀티프로세싱과 멀티스레딩 사이의 또 다른 차이는 공유 상태를 사용하는 기술에 있다. 프로세스에 사용할 수 있는 기술을 스레드도 모두 사용할 수 있는 한편, 스레드 같은 메모리 영역을 사용해서 상태를 공유할 수 있다는 장점이 있다.
더 자세히 설명하면, 프로세스는 전용 메모리를 갖는다. 그리고 다른 프로세스는 이를 읽거나 수정할 수 없다. 따라서 프로세스 메모리를 사용해 다른 프로세스와 무언가를 공유하기란 그리 쉽지 않다. 하지만 스레드는 다르다. 같은 프로세스 내의 모든 스레드는 같은 프로세스의 메모리에 접글할 수 있다. 그러므로 스레드들은 공유 상태를 저장하리 위해 같은 프로세스의 메모리를 사용할 수 있다.
하단엔 프로스세 간 공유 상태를 접근할 때 사용하는 기술들이다.
파일 시스템(file system) - 프로세스 사이에서 데이터를 공유하는 가장 간단한 방식으로 볼 수 있다. 이 접근법은 아주 오래되었으며 거의 모든 OS 에서 지원한다. 한 가지 예는 S/W 프로젝트에서 많은 프로세스가 읽는 구성 파일(configuration file)이다. 만약 프로세스 중 하나가 파일을 읽으려고 한다면, 데이터 경쟁 및 동시성 관련 문제를 방지하기 위해 동기화 기법을 사용해야 한다.
메모리 맵 파일(memory-mapperd file) - 모든 POSIX 호환 OS 및 MS WINDOW 에서는 디스크에 있는 파일에 매핑되는 메모리 영역이 있다. 이 메모리 영역을 읽거나 수정할 수 있는 여러 프로세스 사이에 해당 영역을 공유할 수 있다. 이 기술은 파일 시스템 접근법과 매우 비슷하지만, 파일 API를 사용하는 파일 서술자(FD, file descriptor)로부터 데이터를 스트리밍할 때 발생하는 문제는 적다. 매핑된 영역에 대합 접근 권한이 있는 프로세스가 이 영역의 내용을 수정할 수 있을 때, 적절항 동기화 메커니즘을 사용해야 한다.
네트워크(network) - 다른 컴퓨터에 있는 프로세스들이 통신하는 유일한 방법은 네트워크 인프라 및 소켓 프로그래밍 API를 사용하는 것이다. 소켓 프로스래밍 API 는 SUS 및 POSIX 표준에서 중요하며, 거의 모든 OS 에 있다. 이 기술에 대한 세부적인 내용은 방대하고, 이를 다루는 책도 많다. 다양한 프로토콜, 아키텍처, 데이터 흐름을 다루는 여러 방법 및 더 자세한 내용이 이 기술의 하위 항목에 속한다.
신호(signal) - 같은 OS 내에서 실행되는 프로세스는 서로 신호를 보낼 수 있다. 이는 명령 신호를 전달할 때 많이 쓰이지만, 작은 상태(페이로드, payload)를 공유할 때도 쓰일 수 있다. 공유 상태의 값은 신호로 전달할 수 있으며 대상 프로세스에서 가로챌 수 있다.
공유 메모리(shared memory) - POSIX 호환 OS 및 MS WINDOW 에서는 여러 프로세스 사이에 겅유되는 메모리 영역이 있다. 그러므로 프로세스들은 이 공유 영역을 사용해 변수를 저장하고 값을 공유할 수 있다. 공유 메모리는 데이터 경쟁으로부터 보호되지 않으며, 따라서 수정 가능한 공유 상태에 대한 자리 표시자로 공유 메모리를 사용하려는 프로세스는 동시성 문제를 피하기 위한 동기화 메커니즘을 사용해야 한다. 공유 메모리 영역은 동시에 여러 프로세스가 사용할 수 있다.
파이프(pipe) - POSIX 호환 OS 및 MS WINDOW 에서 파이프는 단방향 통신 채널이다. 파이프는 두 프로세스 간에 공유 상태를 전송할 때 사용할 수 있다. 한 프로세스는 파이프를 작성하고 다른 하나는 파이프로부터 읽는다. 파이프는 기명 또는 익명일 수 있으며, 각 파이느는 고유한 유스케이스가 있다.
UNIX 도메인 소켓(UNIX domain socket) - POSIX 호환 OS 및 최근의 WINDOW 10 에는 UNIX 소켓이라는 통신 단말이 있다. 동일한 머신에 있고 동일한 OS 내에서 실행되는 프로세스들은 UNIX 소켓을 사용해서 전이중 채널(full-duplex channel) 을 통해 정보를 전달할 수 있다. UNIX 도메인 소켓은 네트워크 소켓과 매우 비슷하지만, 모든 데이터는 커널을 통해 전송되며 따라서 소켓은 데이터를 전송하는 아주 빠른 방식을 제공한다. 멀티프로세스는 공유 데이터를 통신하기 위해 같은 UNIX 도메인 소켓을 사용할 수 있다. UNIX 도메인 소켓 또한 동인한 머신에 있는 프로세스 간 파일 서술자를 전송하는 등의 특이한 유스케이스도에도 사용될 수 있다. UNIX 도메인 소켓의 좋은 점은 네트워크 소켓인 것처럼 동일한 소켓 프로그래밍 API 를 사용해야 한다는 점이다.
메시지 대기열(message queue) - 거의 모든 OS에 있는 것으로 많은 메시지를 송수신하는 여러 프로세스가 사용하는 커널에서 유지 관리한다. 프로세스는 서로에 대해 알 필요가 없으며, 메시지 대리열에 대한 접근 권한만 있으면 충분하다. 이 기술은 머신에 있는 프로세스가 서로 통신할 수 있도록 할 때만 쓰인다.
환경 변수(environment variable) - UNIX 계열 OS 및 MS WINDOW OS 자체에서 보관하는 일련의 변수를 제공한다. 이러한 변수를 환경 변수라고 하며, 환경 변수는 시스템 내의 프로세스에 접근할 수 있다.
여러 스레드 및 프로세스를 동기화하는 제어 기술에 관해, 멀티프로세싱과 멀티스레딩 환경에서 쓰이는 기술이 POSIX 표준이 제공하는 것과 매우 유사한 API 를 공유한다.
[멀티스레딩]
- 동시 환경에서 병렬 작업을 수행하기 위해 사용자 스레드를 이용하는 것이다. 현재에 와선 단일 스레드만 있는 프로그램을 발견할 확률은 희박하며, 앞으로 접하게 될 거의 모든 프로그램은 멀티 스레드이다. 스레드는 포스세스 내에서만 존재할 수 있고, 소유자 프로세스가 없는 스레드란 있을 수가 없다. 각 프로세스는 최소한 하나의 스레드를 가지는데 이를 메인 스레드(main thread)라고 한다. 단일 스레드를 사용해 모든 작업을 수행하는 프로그램은 단일 스레드(single-threaded) 프로그램이라고 한다. 프로세스 내의 모든 스레드는 같은 메모리 영역에 접근할 수 있어 멀티프로세싱처럼 데이터를 공유하기 위한 복잡한 경우를 생각할 필요가 없다.
스레드는 프로세스와 매우 비슷할 만큼 프로세스가 상태를 공유하거나 전송할 때 사용하는 모든 기술을 사용할 수 있어 스레드 간에 공유 상태에 접근하거나 데이터를 전송할 때 필요한 기술들을 사용한다. 이는 프로세스와 비교했을 때 같은 메모리 영역에 접근할 수 있다는 한 가지 장점이 더 있는 것이다. 따라서 여러 스레드 간에 데이터를 공유하는 일반적인 방법의 하나는 어떤 변수를 선언해서 메모리를 사용하는 것이다.
각 스레드는 고유한 스택 메모리가 있으므로 공유 상태를 유지하는 자리 표시자로 해당 메모리를 사용할 수 있다. 스레드는 스택 내부의 어딘가를 가리키는 주소를 다른 스레드에 전달할 수 있으며, 다른 스레드는 이 주소에 쉽게 접근할 수 있다. 이러한 메모리 주소는 모두 프로세스의 스택 세그먼트에 속하기 때문이다. 스레드는 또한 프로세스가 소유한 동일한 힙 공간에도 쉽게 접근할 수 있으며, 스레드의 공유 상태를 저장하는 자리 표시자로 힙 공간을 사용할 수 있다.
동기화 기술 역시 프로세스가 사용하는 기술과 매우 비슷하다. POSIX API 도 프로세스와 스레드 사이에서 같다. POSIX 호환 OS 가 프로세스와 스레드를 거의 같은 방식으로 다루기 떄문이다.
참고로 WINDOW 는 MS WINDOW 가 POSIX 스레딩 API(pthread)를 지원하지 않는다. WINDOW 는 스레드를 생성하고 관리하는 고유한 API가 있다. 이 API 는 Win32 네이티브 라이브러리에 속하며, 이 API 를 다루는 여러 문서를 웹에서 살펴볼 수 있다.