node.js같은 server-side framework에서 자주 쓰이는 concurrency 방법이다.
Event-based Concurrency는 2가지 부분을 다루게 된다.
따라서 이번에 살펴볼 내용은 어떻게하면 thread 없이 concurrent server를 만들 수 있을까? 하는 내용이다.
Event-based Concurrency는 간단하게 생각해서 어떤 일이 일어날 때 까지 기다리는 것이다.
그리고 그 일이 발생하면 event의 type에 따라서 행동을 수행한다.
위의 그림처럼 event loop라는 것을 통해 발생한 event에 따라서 event handler로 동작을 수행한다.
따라서 어떤 event를 수행 할 것인지에 대한 행동이 마치 스케줄링과 같은 기능을 한다.
select(), poll()이 이벤트가 도달했을 때 우리가 원하는 대로 동작할 수 있게 만들어준다.
Blocking vs Non-Blocking interface
Blocking(synchronous) interface는 caller에게 리턴을 하기 전에 작업을 모두 끝내고 리턴한다.
반면에 Non-blocking(asynchronous) interface는 작업을 하지만 그 즉시 리턴을 한다. 즉 어떤 작업이든 background에서 작업을 한다.
select()함수는 위와같이 생겼으며 간단한 예시는 아래와 같다.
event-based application에서는 thread가 하나뿐이고, 이벤트에 따라서 처리를 하기 때문에 lock도 필요가 없기 때문에 concurrent program에서 나타나는 문제들이 없다.
하지만 만약 block되는 system call을 호출하는 event가 발생한다면 어떻게될까...?
예를들어 클라이언트가 서버에서 디스크로부터 파일을 읽어서 내용을 리턴해주는 경우를 생각해보자.
이 작업을 위해서 파일을 open()하고 read()해야한다. 특히나 open(), read()는 storage시스템에 I/O 요청을 해야하므로 시간이 꽤나 걸리는 작업이다.
thread-based server에서는 이 작업이 문제가되지 않는다. thread1이 파일을 열고, 읽는동안 다른 thread2가 프로그램을 구동하면 되기 때문이다.
하지만 event-based server에서는 thread가 하나뿐이기 때문에 block이 발생하고, 모든 서버의 작업이 block되는 동안은 멈추게된다...
이 문제를 해결하기 위해서 현대의 OS들은 asynchronous I/O라는 I/O 요청방식을 고안해냈다.
이 방법은 I/O request가 왔을 때 그 즉시 control을 caller에게 반환하고 또다른 interface들이 I/O request가 완료되었는지 아닌지를 결정할 수 있도록 한다.
예를들어 Mac에서 struct aiocb Asynchronous I/O control block이라는 용어로 잘 알려진 자료구조가 있다.
이 자료구조를 채운 다음 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가 완료되었는지를 물어보지 않도록 한다.
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