동시성 (Concurrency) 이란 무엇일까? - 부제 : 동시 실행이라는 착각
제목 : 동시성 (concurrency) 는 무엇일까?
이 글을 작성하게 된 계기
프로세스와 스레드의 관계, 그리고 이들을 이루는 정보의 집합을 알아보았으며,
또한, OS 스레드와 사용자 라이브러리 스레드가 서로 다르다는 것을 알게 되었다.
나는 이전에 "프로세스와 스레드" 주제로 글을 다루었는데,
이전에 운영체제에 대해 갖고 있던 잘못된 편견이 바로,
"프로세스가 할당된 프로세서는 해당 프로세스만 실행한다" 였다.
본래 프로세스와 스레드의 시간 분할 실행은 확실하게 맞는 표현이지만,
이는 물리적으로 1 개의 프로세서만 존재 할 때의 일이라는 것이다.
실제로는, 운영체제를 관장하는 "커널 (Kernel)" 이,
하나의 프로세스에 할당된 많은 스레드들을 감지하여, 이를 여러 프로세서에 나누어서 실행 해 준다.
즉, 현재 멀티 프로세서가 당연한 시대에서, 시간 분할 예시는 좀 더 생각해서 받아들일 필요가 있다.
하지만, 컴퓨팅 세상에서 연산 실행에 대한 카테고리는 단순히 동시성, 병렬성으로 나눌 수 없었다.
컴퓨터 과학 용어에는, 동기, 비동기에 대한 개념도 존재한다.
나는 사실 동기, 비동기에 대한 개념을 먼저 알고 있었다. 자바스크립트를 공부했기 때문이다.
그러나, 프로세스와 스레드의 상태와 이동, 시간 분할을 안다고 해서,
(동시성/병행성) 과, (병렬성) 을 이해하고 있다고 말할 수는 없었다.
그리고 현재 뜨고 있는 모던 프로그래밍 언어들이 존재하는데,
이 언어들은 특정 언어의 단점을 해소하고, 문법적 어려움을 최소화 한 언어인 경우가 많았다.
그리고 이러한 최신 언어들은, 비동기 도입, 그리고 "동시성" 에 대한 개념을 내세우고 있었다.
공식 문서에서도 말할 정도로 좋은 기능인 "언어적 차원의 동시성" 이, 왜 좋은지 알 수 없었다.
좋은게 좋은거라고 넘길 수도 있겠지만, 나는 이 개념이 얼마나 중요한지 알고 싶었다.
동시성/병행성 이란?
병렬성과 비슷하게,
컴퓨터 과학에서 여러 계산을 동시에 수행하는 시스템의 특성으로,
잠재적으로는 서로 상호 작용이 가능하다.
위의 위키백과 정의를 보건대 예상되는 것은,
프로세스와 스레드를 의미하는 것이라고 생각한다.
프로세스 내에 스레드는 최소 1개가 존재하고, 컴퓨터 용어로는 종종 스레드를 프로세스라고 부르기도 한다.
결국, 프로세스(Process) 는 생성, 준비, 대기, 실행, 완료 등등의 상태가 존재하지만,
이는 내부에서 실행되는 스레드(Thread) 또한 마찬가지라는 의미이다.
다중 프로세서로 병렬 수행하지 않고 동시성을 사용하는 이유는?
먼저, 나는 20 대 중후반으로, window 98 을 처음으로 사용하기 시작한 사람이다. (2025 년 기준.)
나의 컴퓨터는 "싱글 코어" 였으며, 이는 프로세서가 1개였다.
모든 프로그램이 꺼진 상태에서야 마우스의 움직임이 그나마 부드러웠고,
인터넷이 켜졌을 경우, 마우스의 움직임은 매우 끊겼다.
그 당시에는 많은 프로그램을 실행하기에 당연히 GUI 의 움직임 또한 끊긴다고 생각했지만,
단일 프로세스가 수행하고 있는 동시성/병행성 원리를 알고 나니, 왜 프로그램이 느려졌는지 알 수 있었다.
이 단일 프로세스는 이러한 이벤트들을 동시에 수행하고 있었다.
- GUI 변화 감지 및 렌더링
- 외부 프로그램을 실행
- 네트워크 처리
- 등등 수많은 연산을 처리하기 위한 프로세스 및 스레드 처리
단일 프로세스는 아주아주 단순하게 생각해 보면, 하나의 도메인 영역에 대한 계산을 수행한다.
특히나, 옛날 컴퓨터는 프로세서 자체가 하나이기 때문에 당연하지 않나 싶었다.
그러나, 우리는 그 당시에도 브라우저를 사용하면서, 동시에 마우스 컨트롤, 키보드 컨트롤, 네트워크를 동시에 수행 했다.
이것이 어떻게 가능했을까?
바로 동시/병행 컴퓨팅 덕분이었다.
각각의 도메인 연산은 병렬 연산을 통해 동시에 일어나는 것 같지만,
실제로 맞긴 하다. 물론, 현대에는 2 가지 개념을 혼합하여 사용한다. (스레드가 프로세서보다 많으므로)
그 시절 1프로세서 시절에는 병행 컴퓨팅을 통해 필요한 연산을 동시에 수행했다.
하나의 프로세서가 어떻게 연산을 동시에 할 수 있나?
옛날에는 컴퓨터가 다~ 해주지 생각하며 지냈었지만,
현재 동시성과 병렬성에 대한 개념을 접하고,
프로세스와 스레드와 관계, 상태, 정보 블록, 시간 분할 연산을 알고 난 뒤
그 당시 하나의 프로세서가 정말 많은 작업을 해 주고 있었구나.. 라고 생각한다.
그도 그럴 것이,
- 마우스의 이동, 키보드 입력, 화면 출력
- GUI 구성
- 어플리케이션 연산
- 네트워크 송수신 및 보안
등등 셀 수 없는 프로세스를 동시에 단 하나의 프로세서가 수행하고 있었다는 말이 된다.
그러나, 나는 더더욱이 이해가 되지 않았다.
"동시" 라는 말은, 찰나의 순간을 포착했을 때, 여러 프로세스(스레드) 가 동시에 연산되고 있는 것을
의미하는 것이 아니었나? 물론 이 말도 맞다.
하나의 프로세서에서 "동시" 는, 프로세서의 연산 시간을 "분할" 하여,
여러 프로세스가 나눠서 사용하고 있다는 말이다.
약간 더 이해를 가미하기 위해 말하자면, 프로세서는 여러 프로세스를 시간 분할하여 실행한다.
먼저 프로세스와 스레드의 관계를 말하자면, 밑의 예시와 같이 들 수 있다.
| 시간 | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
----------
| | |
| | 스레드1 |~~~~~~~| |~~~~~~~~~~~~
| 프로세스 | |
| |-------------------------------------------------
| | |
| | 스레드2 | |~~~~~~~~~~~~~~~~~~~~|
| | |
프로세스는 1 개 이상의 스레드를 가지고 있으며, 다중 스레드에 대해서 시간 분할 실행을 한다.
어? 그렇다면, 프로세스 또한 내부의 스레드에게 자신이 할당받은 시간을 분할한다는 말이 된다.
여기서, 1 개의 프로세서가 2 개의 프로세스를 할당받았을 때의 상황을 보자.
| 시간 | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
-------------------------------------------------------------------------
| | |
| | 스레드1 |~~~| |~~~~~~~|
| 프로세스 1| |
| |-------|
| | |
| | 스레드2 | |~~~~~~|
| | |
-------------------------------------------------------------------------
| | |
| | 스레드1 | |~~~~| |~~~~|
| | |
| |-------|
| | |
| 프로세스 2| 스레드2 | |~~~~~~~~|
| | |
| |-------|
| | |
| | 스레드3 | |~~~~~|
| | |
이는 직접 작성 해 본 예시인데,
"시간" 이라는 자원을 상위 자원이 분배한다고 가정했을 때,
프로세서(물리) -> 프로세스(논리) -> 쓰레드(논리)
이러한 시간 분배 작전으로, 수 많은 일들을 "동시에" 하고 있었던 것이다.
아주아주 정확히 말하자면, "동시에 하는 것 처럼" 하고 있었던 것이다.
그러다가 중간에 상당한 양의 논리 연산이 필요한 "스레드" 가 생겨난다면,
가끔씩 프로그램이 느려지거나, 마우스가 동에 떴다 서에 떴다 하던 것이다.
이제야 이해가 간다.
위에서 예시로 들은 입출력 기계, 네트워크, 어플리케이션들은,
병행성, 즉, 동기성을 통해 서로가 시간을 분배하며 사용하며, 서로가 소통한다.
예시 :
- 마우스 기기에서 움직임에 대한 데이터 정보를 받음, GUI 프로세스가 연산하여 마우스 렌더링 위치 조정
- 브라우저는 네트워크를 통해 특정 웹 사이트에서 페이지 정보를 요청. 네트워크 프로세스 응답 이후 렌더링 시작
- 백엔드 어플리케이션 시작(실행 후 슬립) -> 요청 네트워크 연산 후 응답 -> 다시 슬립
이렇게 시간을 분배하여 사용하고, 서로 소통한다는 의미는, "프로세스" 자체라기보다는,
"스레드" 가 더 정확하다. 프로세스의 실질 구성원은 "스레드" 이기 때문이다.
동기성(병행성) 에서 나타날 수 있는 문제
우리가 동시에 실행해야 하는 프로그램들(스레드들) 이 존재하고,
이를 제한된 수의 프로세서에서 원활히 실행하기 위해서는 동기성 개념이 필수적이다.
우리는 동일한 알고리즘의 계산을 수행하기 위해 동기성을 사용하지 않는다.
다양한 카테고리의 프로그램(프로세스 및 스레드)을 실행하여, 서로 상호작용 하는 형태로 실행한다.
이 과정에서 가장 대표적인 것이,
- 브라우저 네트워크 요청 후 대기(Block)
- 백엔드 프로그램 의존성 구축 및 서버 실행 후 요청 대기(Block)
- 등등..
스레드가 다른 스레드의 응답을 기다린다는 예시를 작성 해 보았다.
"즉, 특정 스레드는, 다른 특정 스레드의 응답을 기다린다는 것이다."
이 과정에서 문제가 발생한다.
1. 교착 상태 (Dead Lock)
이는 리소스(물리)적인 문제보단, 논리적인 문제이다.
한번, 교착 상태에 빠진 작업(스레드) 의 그래프 예시를 들어보겠다 :
---
title : 교착 상태 예시 (Dead Lock)
---
flowchart LR
Task1("Task1")
Task2("Task2")
Task3("Task3")
Task4("Task4")
Task1 -- 요청 후 대기 --> Task2
Task2 -- 요청 후 대기 --> Task3
Task3 -- 요청 후 대기 --> Task4
Task4 -- 요청 후 대기 --> Task1
Task5 -- 요청 후 대기 --> Task3
Task6 -- 요청 후 대기 --> Task4
위의 Task 들은 모두 응답을 받아야만 프로그램을 완료하고
자신을 요청하고 있는 다른 자원들에게 응답할 수 있는 상황이다.
즉, 정말 골치아픈 상황이다.
여기 중 하나라도 단순 계산을 통한 응답이라면, 비동기 방식으로 해결할 수 있겠지만,
안타깝게도 정말 타겟 스레드의 응답만을 바라는 상태라면, 이는 해결 할 수 없는 문제이다.
여기서 교착 상태의 조건이 4 가지가 존재한다 :
- 상호 배제 : 프로세스들이 필요로 하는 자원에 대해 "배타적인" 통제권을 요구한다. - 해당자원내꺼
- 점유 대기 : 프로세스가 할당된 자원을 가진 상태에서 다른 자원을 기다린다.
- 비선점 : 프로세스가 어떤 자원의 사용을 끝낼 때 까지 해당 자원을 뺏을 수 없다.
- 순환대기 : 각각의 프로세스는 "순환적" 으로 다음 프로세스가 요구하는 자원을 가지고 있다.
이 조건이 "모두" 만족해야 교착 상태가 발생한다.
그러나, 현대의 대부분의 운영 체제들은 4 번째 조건을 막으면서 교착 상태를 방지한다고 한다.
2. 기아 상태 (Starvation)
동기성/병행성 시스템은 시간 분할을 통해, 모든 작업을 동시에 실행한다.
그런데, 지속적으로 필요한 컴퓨터 자원을 가져오지 못하는 상황(기아 상태) 가 일어날 수 있다고 한다.
왜 그럴까?
이는 주로 낮은 품질(제대로 만들어지지 않은) 멀티태스킹 시스템에 의해 발생한다고 한다.
사용자의 OS 는 다양한 목적에 의해 수많은 프로세스들이 만들어지는데,
예를 들어서 화면 출력과 키보드 입력, 마우스 입력이 존재한다.
이러한 프로세스들은 끊임없이 필요한 자원을 요구한다. 변화를 보여주어야 하기 때문이다.
그러나, 스케줄링 시스템이 잘못 설계된 경우, 필요한 자원을 지속적으로 응답하지 못할 가능성이 높다고 한다.
그리고 옛날 방식의 공격이 있는데, Fork Bomb(포크 폭탄) 이라고 하는 공격에 의해 발생할 수도 있다고 한다.
프로세스는 스스로를 fork 하여 복제 할 수 있는데,
특정 프로세스가 스스로를 여러개로 복제하고, 또 복제된 프로세스들이 스스로를 여러개로 복제하는 공격이다.
이렇게 되는 경우, 너무 많은 프로세스들이 존재하게 되어 끊임없이 자원을 필요로 하는 프로세스가 제때 응답을 받지 못한다고 한다.
이를 "서비스 거부 공격" 이라고 말한다.
그래서 동기성/병행성을 요약해서 말하자면?
프로세서보다 많은 프로세스를 "동시에" 수행할 수 있다.
그러나, 진정한 의미의 "동시에" 가 아니라, 프로세서가 시간 분할 알고리즘에 따라
각각의 프로세스를 나눠서 실행한다.
다른 개념인 병렬성이란?
병렬성이란, "동시에 실행 하는 것 처럼 보이는 것" 이 아니라,
진정한 의미로 "동시에 실행" 하는 것을 의미한다.
병렬 컴퓨팅을 수행하기 위해서는 현재 실행하는 프로그램만큼, 프로세서가 존재해야 한다.
따라서, 슈퍼 컴퓨터, GPU 와 같은 장치에서 가장 중요하게 다뤄지는 개념이다.
그러나, 제한된 리소스를 최대한 활용하기 위해 사용되는 동기성/병행성 개념과 달리,
같은 연산, 혹은 프로그램을 병렬 연산하는 과정에서 자원 분배, 경쟁에 대한 논리가 더 복잡하다.
동시성이 좋은 언어가 있다?
TypeScript 의 파싱 과정을 Go 로 대체하는 과정에서 Golang 공식 홈페이지에 들어가 보았는데,
자신의 언어가 동시성에 매우 좋다는 것을 어필했었다.
여기서 잠깐, "동시성이 좋다" 라는 의미에 대해서 짚고 넘어가야 할 필요가 있다고 생각한다.
대부분의 언어는 OS 스레드와, 사용자 라이브러리 스레드를 1 : 1 매칭시켜 실행한다.
즉, 우리가 만드는 Node.js 의 Worker
이나,
Java 언어에서의 Runnable
, 혹은 Thread
와 같은 것들은 1 : 1 매칭이다.
그리고, OS 스레드들은는 커널에 의해 훌륭한 시분할 알고리즘을 사용하여 실행되고,
이들은 결국 동시성이 고려된 상태로 실행된다. 즉, 동시성이 좋다.
또 잠깐, 그렇다면, 결국 대부분의 언어는 동시성을 사용할 수 있다는 말이 아닌가?
맞다. 대부분의 언어, 말하지 않았던 고수준의 언어인 Python 까지도 OS 레벨 스레드를 생성하여
동시성을 고려한 프로그래밍이 가능하다.
그런데, 왜 특정 언어에서는 "동시성이 좋다" 라고 말하는 것일까?
나도 이전에 작성했던 "프로세스와 스레드에 관하여" 블로그 글을 작성하면서 알게 되었던 것인데,
Golang 과 같은 언어에서 동시성 전용 메서드 코드 혹은 struct 와 같은 용어를 사용한다면,
이들은 컴파일 과정에서 OS 레벨 스레드 M
개, 사용자 레벨 스레드 N
개가 생성되어
이들을 "멀티플렉싱" 하여 실행한다.
주로 특징이, 프로세스가 컨텍스트 스위칭(상태변환 EX - 실행, 블록, 대기, 준비 등등)
과정에서는 그 뒤의 과정에 멈춘다. 그러나, 이런 멀티플렉싱 기법을 통하여,
자주 일어나는 컨텍스트 스위칭 시간조차도 활용하여 남은 과정을 실행한다는 특징이 있다.
Go 언어에서는 자체적인 M : N
매칭 알고리즘을 만들어 놓았는데, 이를 goroutine 이라고 부른다.
Rust 언어에서도 동기성을 위해 특정 기능을 넣어놓을 것으로 보인다. 하지만,
Rust 언어의 "빌림(Borrow)" 기능에 대해서 잘 이해하지 못하는 상황에서 설명하기는 어려워 보인다.
이 글을 작성하면서 배운 점은
결국 시분할 실행 시스템과 동시성/병행성 은 뗄려야 뗄 수 없는 거의 동등한 관계라는 것을 알게 되었다.
무엇보다, 이 주제를 다루면서 너무나도 어릴 적 형의 컴퓨터를 사용할 때, 버벅이는 이유를 알게 되었다.
왜 인터넷 브라우저를 실행하고 나서 마우스를 움직이면 버벅였는지,
그때 당시에는 프로그램 하나를 켜 놓고, 꼭 완료 될 때 까지 기다리는 습관이 있었는데,
아마 이를 몸으로 체득하고 기다리지 않았을까 생각한다.
한정된 리소스인 프로세서에서 그보다 훨씬 많은 프로세스와 스레드들을 실행하기 위해,
"동시에 실행" 이라는 개념을 비틀어서 분할하여 동시에 실행이라는 개념을 알게 되었다.
거꾸로 생각해 보니, 예를 들어
코드를 작성하면서, 이 코드를 설명하는 마크다운 문서를 작성하면서, 소통하는 중이라고 가정 해 보자.
"나" 라는 사람은 뇌가 1개 뿐이다. 하지만, 나는 이 3 개의 작업을 하고 있다.
물론, 코드를 잠깐 작성하고, 시선을 돌려 문서를 작성하며, 프로그램을 켜서 슬랙으로 소통하고 있다.
누군가 와서, "당신은 지금 무슨 일을 하고 있습니까?" 라고 물어본다면,
나는
- Task1 : 코드를 작성하며,
- Task2 : 이 코드가 작성되는 대로 이에 대한 문서를 작성하며,
- Task3 : 진행도에 대한 내용을 팀원과 소통하고 있다.
이 세 가지를 "동시에" 하고 있다고 설명 할 것이다.
참조 사이트
위키백과 (Concurrency - 병행성 == 동시성)
https://ko.wikipedia.org/wiki/%EB%B3%91%ED%96%89%EC%84%B1
위키백과 (낙관적 병행 수행 제어)
위키백과 (병렬 컴퓨팅 or 병렬 연산 - Parallel Computing)
https://ko.wikipedia.org/wiki/%EB%B3%91%EB%A0%AC_%EC%BB%B4%ED%93%A8%ED%8C%85
위키백과 (Task Parallelism - 작업 병렬성)
https://ko.wikipedia.org/wiki/%EC%9E%91%EC%97%85_%EB%B3%91%EB%A0%AC%EC%84%B1
위키백과 - 교착 상태
https://ko.wikipedia.org/wiki/%EA%B5%90%EC%B0%A9_%EC%83%81%ED%83%9C
위키백과 - 기아 상태
https://ko.wikipedia.org/wiki/%EA%B8%B0%EC%95%84_%EC%83%81%ED%83%9C