쓰레드를 사용하는 것이 병행 프로그램을 제작하는 유일한 방법은 아니다. 이벤트 기반의 병행성(Event-based Concurrency) 스타일로 프로그래밍을 하여 병행 프로그램을 작성할 수 있다.
이벤트 기반의 병행성(Event-based Concurrency)
node.js
프레임워크가 이벤트 기반의 병행성 스타일을 사용하여 만들어졌다.핵심 질문: 어떻게 쓰레드 없이 병행 서버를 개발할까?
- 쓰레드 없이 병행 서버를 구현할 때, 어떻게 병행성을 유지하면서 각종 문제들을 피할 수 있을까?
이벤트 기반의 병행성(Event-based Concurrency)
while (1) {
events = getEvents();
for (e in events)
processEvent(e);
}
그렇다면 발생한 이벤트가 무슨 이벤트인지 어떻게 판단할까?
- 네트워크나 디스크 I/O의 경우 쉽지 않음
- 이벤트 메세지가 자신을 위한 것인지 어떻게 판단?
select()
(or poll()
)기본질문: 이벤트를 어떻게 받을까?
select()
또는 poll()
시스템 콜을 기본 API로서 제공한다.select()
API
int select(int nfds ,
fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict errorfds,
struct timeval *restrict timeout);
nfds
: 파일 디스크립터들의 개수 (0~nfds-1
)readfds
,writefds
,errorfds
: 읽기/쓰기/에러 파일 디스크립터 집합timeout
: 일반적으론 NULL(준비 완료까지 대기) 또는 0(즉시 리턴)으로 설정select
는 집합을 가리키는 각 포인터들을 준비된 디스크립터들의 집합으로 교체한다. select
를 이용하면 디스크립터에 대한 읽기/쓰기 여부를 검사할 수 있다.select()
의 사용다음은 select()
를 이용해 어떤 네트워크 디스크립터에 메시지가 도착 했는지를 파악하는 예제 코드이다.
#include <stdio . h>
#include <stdlib . h>
#include <sys/time . h>
#include <sys/types . h>
#include <unistd . h>
int main(void) {
// 여러 개의 소켓을 열고 설정(여기엔 나타나있지 않음)
// 주 반복문
while () {
// fd_set을 모두 0으로 초기화
fd_set readFDs;
FD_ZERO(&readFDs);
// 이제 이 서버가 관심있어하는 디스크립터들의 bit를 설정
// (단순함을 위해서 min~max로 설정)
int fd;
for (fd = minFD; fd < maxFD; fd++)
FD_SET(fd , &readFDs);
// 선택을 함
int rc = select(maxFD+1 , &readFDs , NULL , NULL , NULL);
// FD_ISSET()을 사용하여 실제 데이터 사용 여부 검사
int fd;
for (fd = minFD; fd < maxFD; fd++)
if (FD_ISSET(fd , &readFDs))
processFD(fd);
}
}
select()
를 호출하여 데이터가 도착한 소켓이 있는지 검사FD_ISSET()
를 사용하여 이벤트 서버는 어떤 디스크립터들이 준비된 데이터를 갖고 있는지를 알 수 있다.processFD(fd)
)한다.팁: 이벤트 기반의 서버 내에서는 block을 하지 말자
- 호출자가 실행한 것을 차단할 수 있는 호출이 있어선 안 된다.
- 만약 block을 한다면, 이벤트 기반 서버가 멈추게 될 것
단일 CPU를 사용하는 이벤트 기반의 응용 프로그램에서는 병행 프로그램을 다룰 때 나타났던 문제들은 더 이상 보이지 않는다.
이벤트 기반의 서버는 단 하나의 쓰레드만 갖고 있기 때문에 다른 쓰레드에 의해서 인터럽트에 걸릴 수가 없다.
-> 병행성 버그는 기본적인 이벤트 기반 접근법에서는 나타나지 않는다.
만약 차단될 수도 있는 시스템 콜을 불러야 하는 이벤트가 있다면 어떻게 할까?
open()
시스템 콜을 사용하여 파일 열기read()
시스템 콜을 사용하여 파일 읽기open/read
가 모두 저장장치에 I/O 요청을 보내야 한다면?비동기 I/O (asynchronuous I/O)
struct aiocb {
int aio_fildes; /* File descriptor */
off_t aio_offset; /* File offset */
volatile void *aio_buf; /* Location of buffer */
size_t aio_nbytes; /* Length of transfer */
};
aio_fildes
: 읽고자 하는 파일의 파일 디스크립터aio_offset
: 파일 내에서 위치aio_buf
: 읽기 결과로 얻은 데이터를 저장할 대상 메모리의 위치aio_nbytes
: 요청의 길이int aio_read(struct aiocb *aiocbp);
int aio_error(const struct aiocb *aiocbp);
aio_read
: 비동기 읽기 APIaio_error
: I/O가 종료되었는지, aio_buf
에 요청했던 데이터가 있는지 알 수 있는 APIaiocbp
에 의해 참조된 요청이 완료되었는지 검사EINPROGRESS
반환aio_error()
시스템 콜로 시스템에 폴링(poll)
하여 해당 I/O가 완료되었는지 확인할 수 있다.문제점: 만약 어떤 시점에 수십 또는 수백 개의 I/O를 요청하는 프로그램이 있다면, 그 많은 요청들을 일일이 다 검사해야 할 것인가 아니면 먼저 일정 시간 동안을 대기해야 할까, 그것도 아니라면?
상태관리
fd
)로 명시된 파일 읽고, 네트워크 소켓 디스크립터(sd
)로 데이터 전송int rc = read(fd, buffer, size);
rc = write(sd, buffer, size);
read()
가 리턴되면 전송할 네트워크 소켓에 관한 정보가 같은 스택에 존재read()
를 비동기로 요청aio_error()
를 사용하여 주기적으로 읽기가 종료되었는지를 확인sd
) 를 파일 디스크립터 (fd
) 가 사용하는 자료 구조 (예 : 해시 테이블) 에 저장하기select()
가, 디스크 I/O 에는 AIO가 사용되고 있다.