[WAFFLE] 04-1 Concurrent Programming
Published:
In this post, concepts of Concurrent Programming will be provided.
Concurrent Programming
Parallelism
- 두 개 이상의 명령어가 동시에 실행되는 것
- 여기서 명령어란, CPU가 실행할 수 있는 최소 단위의 명령어를 의미한다.
- 산술 명령어, 메모리 접근 명령어 등.
- 두 개 이상의 명령어가 동시에 실행되려면 CPU 안에 코어가 여러개 있어야 함. 하나의 코어는 한 순간에 하나의 일만 할 수 있기 때문.

- 엄밀하게 얘기하면 CPU 내부에서 pipelining 이라는 과정을 거쳐 코어가 하나여도 병렬성을 구현할 수 있음. 컴퓨터 구조, 시스템 프로그래밍 수업에서 다룸.
- 여기서는 하나의 코어는 한 순간에 하나의 일만 할 수 있음을 전제로 함.
Concurrency
- 두 개 이상의 작업이 동시에 실행되는 것
- 작업은 여러 개의 명령어로 이루어져 있으며, 이 명령어들이 동시에 실행되는 것을 의미하는 것이 아니다.
- 병렬성을 갖지 못하더라도 동시성은 갖을 수 있음. 왔다갔다 하면서.
- 병렬은 동시를 포함하는 개념. 병렬성을 가지면 동시성을 가짐.
- 각 명령어들이 순서에 관계 없이, 혹은 부분적인 순서만을 가지고 실행되는 것을 의미한다.
- line 1->2->3 … 순서대로 실행되지 않음.
- 예컨데, 와팡 서버에 2개의 주문 취소 요청이 동시에 들어왔을 때, 동시성을 지원하는 서버에서는 아래와 같이 처리하는 것이 가능하다.
- 요청 1의 주문 찾기 (요청1 / 명령어1)
- 요청 2의 주문 찾기 (요청2 / 명령어1)
- 요청 1의 주문 상태 -> CANCELED로 변경 (요청1 / 명령어2)
- 요청 2의 주문 상태 -> CANCELED로 변경 (요청2 / 명령어2)
Concurrency Problem

- 5명의 철학자가 원형 테이블에서 식사 중
- 이 음식은 반드시 2개의 포크를 사용
- 단순한 알고리즘
- 왼쪽 포크가 사용 가능할 때까지 생각하기
- 왼쪽 포크 집어들기
- 오른쪽 포크가 사용 가능할 때까지 생각하기
- 오른쪽 포크 집어들기
- 일정 시간 동안 먹기
- 왼쪽 포크 내려놓기
- 오른쪽 포크 내려놓기
- 단순한 알고리즘대로라면, 모두가 왼쪽 포크를 집어든 순간 교착 상태가 발생
- 단순한 해결책 : 플라톤만 오른쪽 포크 먼저 집기
- 나머지 4명의 철학자가 왼쪽 포크를 집었을 때, 플라톤은 오른쪽 포크를 집지 못하므로 교착 상태에 빠지지 않음
- 동시성 프로그래밍을 할 때 단순히 알고리즘을 짜면 여러가지 문제 발생. 이를 해결하기 위한 여러가지 알고리즘 존재.
Process and Thread

- 프로그램을 실행을 하면 원래는 코드형태로 있던 프로그램이 컴파일이 되어 binary executable로 만들고 이를 터미널에서 오픈하든, gui에서 열든 함. 그렇게 실행을 할 때 Process가 생성이 됨.
- 클래스와 인스턴스의 관계와 같음. 실행가능파일이 클래스라면 그걸 통해 만들어진 프로세스가 클래스 인스턴스인 셈이다.
- 프로세스는 기본적으로 한 순간에 하나의 명령어만 실행할 수 있음. 프로세스 안에 명령어들이 저장이 되어 있는데 이들을 동시에 실행할 수 없음. 만약에 우리가 같은 코드를 동시에 실행하고 싶으면 프로세스를 두 개 띄워야함. 그러면 운영체제가 알아서 Process 2개가 왔다갔다 실행되도록 스케줄링 해줌. 그러나 Process를 한 번 열때마다 오버헤드가 큼. 따라서 이보다 좋은 해결책을 찾았는게 그것이 바로 스레드임.
- 스레드는 프로세스 안에서 여러개 실행될 수 있는데 웬만한 것은 전부 공유하고 스택 프레임만 별도로 가지고 있음.
- 파이썬에서는 global lock이 있어 멀티 코어 CPU라 하더라도 동시에 여러 코드를 실행하지 않음. 파이썬은 프로세스를 여러개 만들어야 함. Uvicorn으로 FastAPI 서버 실행시킬 때 walker 옵션을 이용하여 몇 개의 프로세스로 띄울지 설정 가능.
I/O
- Input / Output
- 디스크 읽기/쓰기, 소켓 읽기/쓰기 등 외부 시스템과의 통신
- I/O 자체는 커널에서 수행되며, Inturrupt 등으로 해당 작업의 완료를 알려줌
- I/O는 코드에서 실행되는 게 아님. 그말은 I/O 작업이 CPU를 방해하지 않는다는 것.

- 우리가 흔히 사용하는 I/O 예를들어, 파이썬에서 Read() 등을 사용할 때 파일에 있는 문자열이 읽어와질 때까지 기다려야 함. 그 다음에야 그 다음 코드를 실행할 수 있음. 그런식으로 I/O가 우리가 실행하는 게 아니라 커널이 실행하는 것임에도 불구하고 I/O가 끝날 때까지 기다리는 I/O를 Blocking I/O라 함.
- I/O 작업이 커널에서 완료될 때까지 기다림
- 해당 I/O 작업을 요청한 스레드는 그 동안 Block

- 더 나아간 방식인 Non-Blocking I/O의 경우, Read 함수를 사용했을 때, 바로 반환이 되고 반환 값이 아무것도 없음. 커널한테 읽으라고 시킨것이고 우리가 루프를 돌면서 주기적으로 직접 체크해야함.
- 엄청 큰 파일이라 읽는데 10초가 걸린다면 다른 작업을 하고 있다가 커널한테 나중에 물어봐서 작업이 끝났다면 그 데이터를 가지고 다음 task 수행 가능.
- I/O 작업이 완료되는 걸 기다리지 않음
- I/O 작업이 완료됐는지 계속 확인해야 함 (Polling)

- I/O 작업이 완료되는 걸 기다리지 않음(논블로킹과 마찬가지)
- I/O 작업이 완료됐는지 확인할 필요 없이 커널에서 시그널을 통해 프로세스에게 알려줌
- uvicorn으로 FastAPI 실행 시키고 끌 때 ctril+c를 누름. 그러면 그 프로세스한테 kill이라는 시그널을 주는 것.
- 기본적으로 파이썬은 몇가지 유용한 시그널들에 대해 언어 자체적으로 시그널 핸들러가 있음. C 언어라면 각각의 시그널들에 대해 직접 핸들러를 작성해서 받아줘야 함. 파이썬은 몇가지 시그널들에 대해 kill 시그널이 들어왔을 때 프로세스를 멈춰라 같은 사전정의 된 핸들러가 정의되어 있음. 운영체제 커널에서 프로세스에 주는 신호를 시그널이라 함.
- I/O 가 완료되었을 때 무슨 작업을 할건지 미리 시그널 핸들러를 정의해서 건네 주어야 함.
완료됐을 때 수행할 작업을 등록 (Callback)
- 논블로킹과 비동기 I/O는 운영체제에서 지원을 해줘야함. 운영체제에 따라 비동기는 지원 안될수도. 그럼에도 불구하고 Non-Blocking I/O가 있다면, Asynchronous I/O를 “구현”할 수 있음
- 별도의 I/O전용 스레드를 만들고, I/O 작업 발생 시 해당 스레드한테 이거 수행하고 그 다음에 핸들러 실행 해, 이렇게 프로그래밍 해두면 시그널을 보내는게 커널이냐 아니냐만 차이날 뿐 큰 차이 없음.

Leave a Comment