[WAFFLE] 04-1 Concurrent Programming

4 minute read

Published:

In this post, concepts of Concurrent Programming will be provided.

Concurrent Programming

Parallelism

  • 두 개 이상의 명령어가 동시에 실행되는 것
  • 여기서 명령어란, CPU가 실행할 수 있는 최소 단위의 명령어를 의미한다.
    • 산술 명령어, 메모리 접근 명령어 등.
  • 두 개 이상의 명령어가 동시에 실행되려면 CPU 안에 코어가 여러개 있어야 함. 하나의 코어는 한 순간에 하나의 일만 할 수 있기 때문.

pipeline

  • 엄밀하게 얘기하면 CPU 내부에서 pipelining 이라는 과정을 거쳐 코어가 하나여도 병렬성을 구현할 수 있음. 컴퓨터 구조, 시스템 프로그래밍 수업에서 다룸.
  • 여기서는 하나의 코어는 한 순간에 하나의 일만 할 수 있음을 전제로 함.

Concurrency

  • 두 개 이상의 작업이 동시에 실행되는 것
  • 작업은 여러 개의 명령어로 이루어져 있으며, 이 명령어들이 동시에 실행되는 것을 의미하는 것이 아니다.
    • 병렬성을 갖지 못하더라도 동시성은 갖을 수 있음. 왔다갔다 하면서.
    • 병렬은 동시를 포함하는 개념. 병렬성을 가지면 동시성을 가짐.
  • 각 명령어들이 순서에 관계 없이, 혹은 부분적인 순서만을 가지고 실행되는 것을 의미한다.
    • line 1->2->3 … 순서대로 실행되지 않음.
  • 예컨데, 와팡 서버에 2개의 주문 취소 요청이 동시에 들어왔을 때, 동시성을 지원하는 서버에서는 아래와 같이 처리하는 것이 가능하다.
    1. 요청 1의 주문 찾기 (요청1 / 명령어1)
    2. 요청 2의 주문 찾기 (요청2 / 명령어1)
    3. 요청 1의 주문 상태 -> CANCELED로 변경 (요청1 / 명령어2)
    4. 요청 2의 주문 상태 -> CANCELED로 변경 (요청2 / 명령어2)

Concurrency Problem

dining-philosophers

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

Process and Thread

process-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를 방해하지 않는다는 것.

blocking-io

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

nonblocking-io

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

asynchronous-io

  • 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