Event-based Concurrency (Advanced)

yalpalyappap·2021년 3월 16일
0

운영체제

목록 보기
20/20

node.js같은 server-side framework에서 자주 쓰이는 concurrency 방법이다.

Event-based Concurrency는 2가지 부분을 다루게 된다.

  1. multi-threaded application에서 concurrency를 정확하게 다루는 것이 어렵다는 것이다.
    lock을 안하거나, deadlock이 발생하거나..
  2. mutil-threaded application에서 개발자가 scheduling을 통제하지 못하거나, 할 수 없다는 점이다.

따라서 이번에 살펴볼 내용은 어떻게하면 thread 없이 concurrent server를 만들 수 있을까? 하는 내용이다.

The Basic Idea: An Event Loop

Event-based Concurrency는 간단하게 생각해서 어떤 일이 일어날 때 까지 기다리는 것이다.

그리고 그 일이 발생하면 event의 type에 따라서 행동을 수행한다.

위의 그림처럼 event loop라는 것을 통해 발생한 event에 따라서 event handler로 동작을 수행한다.

따라서 어떤 event를 수행 할 것인지에 대한 행동이 마치 스케줄링과 같은 기능을 한다.

An Important API: select() (or poll())

select(), poll()이 이벤트가 도달했을 때 우리가 원하는 대로 동작할 수 있게 만들어준다.

Blocking vs Non-Blocking interface
Blocking(synchronous) interface는 caller에게 리턴을 하기 전에 작업을 모두 끝내고 리턴한다.
반면에 Non-blocking(asynchronous) interface는 작업을 하지만 그 즉시 리턴을 한다. 즉 어떤 작업이든 background에서 작업을 한다.

Using select()


select()함수는 위와같이 생겼으며 간단한 예시는 아래와 같다.

Why Simpler? No Locks Needed

event-based application에서는 thread가 하나뿐이고, 이벤트에 따라서 처리를 하기 때문에 lock도 필요가 없기 때문에 concurrent program에서 나타나는 문제들이 없다.

A Problem: Blocking System Calls

하지만 만약 block되는 system call을 호출하는 event가 발생한다면 어떻게될까...?

예를들어 클라이언트가 서버에서 디스크로부터 파일을 읽어서 내용을 리턴해주는 경우를 생각해보자.

이 작업을 위해서 파일을 open()하고 read()해야한다. 특히나 open(), read()는 storage시스템에 I/O 요청을 해야하므로 시간이 꽤나 걸리는 작업이다.

thread-based server에서는 이 작업이 문제가되지 않는다. thread1이 파일을 열고, 읽는동안 다른 thread2가 프로그램을 구동하면 되기 때문이다.

하지만 event-based server에서는 thread가 하나뿐이기 때문에 block이 발생하고, 모든 서버의 작업이 block되는 동안은 멈추게된다...

A Solution: Asynchronous I/O

이 문제를 해결하기 위해서 현대의 OS들은 asynchronous I/O라는 I/O 요청방식을 고안해냈다.

이 방법은 I/O request가 왔을 때 그 즉시 control을 caller에게 반환하고 또다른 interface들이 I/O request가 완료되었는지 아닌지를 결정할 수 있도록 한다.

예를들어 Mac에서 struct aiocb Asynchronous I/O control block이라는 용어로 잘 알려진 자료구조가 있다.

  • 읽어야 하는 file discriptor(aio_fildes)
  • file의 offset(시작지점) (aio_offset)
  • 읽어야 하는 길이 (aio_nbytes)
  • 읽은 파일을 저장 할 버퍼 (aio_buf)

이 자료구조를 채운 다음 asynchronus read API를 호출한다.

int aio_read(struct aiocb *aiocbp)

이 API를 호출 한 이후 그 즉시 control을 리턴하고, 작업은 background에서 진행되고 서버는 이어서 동작한다.

그렇다면 I/O 작업이 끝났음을 어떻게 알 수 있을까?

int aio_error(const strunct aiocb *aiocbp)

위의 API가 I/O 작업이 끝났다면 0을 리턴하고, 그렇지 않다면 EINPROGRESS를 리턴한다.

여기서 볼 수 있는 문제점은 만약 수백만 건의 I/O request가 동시에 온다면 그 모든 요청에 대해서 결과를 확인하는데도 엄청난 반복을 해야한다....

따라서 이 문제를 조금 완화하기 위해서 시스템에서는 interrupt를 허용한다. I/O가 완료되었을 때 signal을 보냄으로써 불필요하게 반복적으로 시스템에 I/O가 완료되었는지를 물어보지 않도록 한다.

Another Problem: State Management

event-based의 또다른 문제점은 전통적인 thread-based 방식보다 코드를 작성하기가 까다롭다는 점이다.

예를들어 event handler가 asynchronous I/O를 요구하고 나중에 그 작업이 완료했을 때 그 다음 event handler가 사용할 프로그램의 상태를 구성해줘야 한다.

프로그램의 상태가 stack에 존재했던 thread-based 방식에서는 처리하지 않아도 됐던 문제가 있는 것이다.

mutual-stack management가 event-based 방식에선 매우 중요하다.

좀 더 구체적으로 예시를 통해 살펴보자.

우선 thread-based 방식에서 데이터를 읽어서 읽은 데이터를 네트워크 소켓에 작성하는 작업을 생각해보자.

int rc = read(fd, buffer, size);
rc = write(sd, buffer, size);

thread-based 방식에서는 어떤 fd를 읽고, 어떤 fd에 write를 해야하는지 명확하기 때문에 까다로울게 없다.

하지만 event-based 방식에서는 AIO call을 통해 read를 asynchronous하게 수행하고, aio_error()를 통해 작업이 완료됐음을 알아야 한다.

그렇다면 작업이 완료되었을 때 event-based server는 어떻게 무엇을 할지 알 수 있을까?

우선 작업을 끝내는데 필요한 정보가 기록된 자료구조를 저장한 후, 끝났다는 event가 발생하면 이 자료구조를 살펴서 event를 처리한다.

위의 예제같은 경우에는 socket의 fd를 자료구조에 저장한 후 read가 끝났을 때 자료구조를 살펴서 행동을 수행 할 것이다.

출처 : OSTEP

profile
안녕하세요! 개발 공부를 하고있습니다~

0개의 댓글