크래프톤정글/운영체제

[OSTEP] 병행성 CH33 - 34

아람2 2024. 11. 4. 23:49
반응형

CH33 이벤트 기반의 병행성 - 고급 

멀티 쓰레드 기반 프로그래밍이 어려운 이유는 아래 두 가지이다 

1) 멀티 쓰레드 기반 프로그래밍은 어렵다 

자료 구조를 락으로 보호하는 것을 잊을 수 있고, 교착 상태나 혹은 다른 문제들이 발생할 수 있다 

2) 개발자가 쓰레드 스케줄링에 대한 제어권을 가지고 있지 않다 

멀티 쓰레드 프로그래밍에서는 운영체제가 CPU 스케줄링에 대한 전권을 갖는다 

핵심 질문 - 어떻게 쓰레드 없이 병행 서버를 개발할까
쓰레드 없이 병행 서버를 구현할 때, 어떻게 병행성을 유지하면서 각종 문제들을 피할 수 있을까?

33.1 기본 개념 - 이벤트 루프 

이벤트 기반의 병행성은 특정 사건, "이벤트" 의 발생을 대기하고, 사건이 발생하면, 사건의 종류를 파악한 후 추후 작업을 진행한다 

이 과정에서 사용하는 구조가 이벤트 루프 Event Loop 이다, 이벤트 루프의 코드는 아래와 같다 

while (1) {
	event = getEvents();
	for (e in events)
		processEvent(e);
}

 

이벤트 기반의 병행성 - 1) 루프 내에서 이벤트 발생을 대기하고, 2) 이벤트가 발생하면 하나씩 처리한다 (이벤트를 처리하는 코드를 이벤트 핸들러라 부른다) 3) 다음에 처리할 이벤트를 결정한다 

이벤트의 처리가 시스템의 유일한 작업이기 때문에, 다음에 처리할 이벤트를 결정하는 것이 스케줄링과 동일한 효과를 갖는다 

🐣 이벤트 처리 과정에서는 하나의 이벤트가 완료될 때까지 다음 이벤트를 처리하지 않기 때문에 하나의 처리만 수행한다, 따라서 각 이벤트가 독립적으로 발생하고 처리되므로, 현재 처리 중인 이벤트가 끝난 후 다음에 어떤 이벤트를 처리할지를 결정하는 것이 스케줄링과 유사한 역할을 한다고 볼 수 있다 🐣

하지만 발생한 이벤트가 구체적으로 어떤 이벤트이고, 자신을 위한 메시지인지 판단은 어떻게 할까? 

33.2 중요 API - select() ( or poll() )

대부분의 시스템은 select() 또는 poll() 시스템 콜을 이벤트 발생 감지 기본 API 로서 제공한다 

이 시스템 콜들은 I/O 사건이 발생했을 때, 처리를 필요로 하는 것이 있는지를 검사한다 

select()는 readfds, writefds, 그리고 errorfds 를 통해 전달된 I/O 디스크립터(descriptor) 집합들을 검사해서, 각 디스크립터에 해당하는 입출력 디바이스가 읽을 준비가 되었는지, 쓸 준비가 되었는지, 처리해야 할 예외 조건이 발생했는지 등을 파악한다, 각 집합의 첫 번째 nfds 개의 디스크립터들, 즉 0부터 nfds-1까지의 디스크립터를 검사한다, select() 는 집합을 가리키는 각 포인터들을 준비된 디스크립터들의 집합으로 교체합니다, select() 는 전체 집합에서 준비된 디스크립터들의 총 개수를 반환힌디

select() 에 대해 알아두어야 할 사항은 1) select() 를 통해 디스크립터에 대한 읽기 or 쓰기 가능 여부를 파악할 수 있다

2) timeout 인자의 값을 0 으로 설정하여 select() 가 즉시 리턴할 수 있게 사용하기도 한다

(+ timeout 인자 1) NULL → 디스크립터에 읽거나 쓸 수 있게 상태가 변경될 때 까지 무한정 대기 2) 0 → 즉시 리턴)

(+ 함수를 사용하는 이유는 모든 입출력을 관리하려고 사용한다)

poll() 의 시스템 콜도 유사하다, 이런 인터페이스들을 사용하여 non-blocking event loop 를 제작한다

이를 통하여 패킷의 도착 여부를 확인하고, 소켓에서 메시지를 읽고, 필요에 따라 답신을 전송한다 

🐣 2) select() 가 즉시 리턴함으로써 프로그램이 필요할 때만 작업을 수행하고, 이벤트 발생 시 적절히 처리할 수 있는 기회를 제공한다

33.3 select() 사용

select()의 역할은 데이터가 도착한 소켓이 있는지 while() 문 무한 루프로 검사한다

🐣 실시간으로 입출력을 관리해야 하니까 무한 루프다!

여담 - 차단 Blocking 과 비차단 Non-Blocking 인터페이스

차단 (또는 동기 Synchronous) 인터페이스는 호출자에게 리턴하기 전에 자신의 작업을 모두 처리한다

비차단 (또는 비동기 Asynchronous) 인터페이스는 작업을 시작하지만, 즉시 반환하기 때문에 처리되어야 하는 일이 B/G 에서 완료된다 

차단 콜 Blocking Call 은 주로 I/O 때문에 발생하고, 비차단 인터페이스 Non-Blocking Interface 는 모든 프로그래밍 스타일에서 사용될 수 있지만, 이벤트 기반 프로그래밍 방식에서는 필수적이다 (차단 방식의 시스템 콜 Blocking Call 이 전체 시스템을 멈출 수 있기 때문)

🐣 차단 호출은 특정 작업이 완료될 때까지 호출한 쓰레드가 대기한다, 비차단 호출은 작업이 B/G에서 처리되고, 작업이 완료되면 이벤트 기반으로 결과를 알려준다, 이벤트 기반 프로그래밍은 주로 이벤트 루프를 통해 실행되며, 이벤트 루프는 단일 스레드로 작동하기 때문에 (한 번에 하나의 작업만 처리할 수 있기 때문에) 작업을 처리할 때 차단 호출을 사용한다면 1) 해당 작업이 완료될 때까지 다른 작업을 처리하지 못 하고 2) 작업이 차단 호출로 실행된다면, 해당 작업이 끝날 때까지 다른 이벤트는 모두 대기 상태에 놓이기 때문에 시스템 전체의 응답성이 떨어지고, 사용자 경험이 크게 저하될 수 있다 

33.4 왜 간단한가? 락이 필요 없음

단일 CPU 에서 이벤트 기반 응용 프로그램을 사용하면, 쓰레드 기반 병행 프로그램에서의 문제가 발생하지 않는다 

1) 한번에 하나의 이벤트만을 처리하기 때문에 락을 획득하거나 해제할 필요도 없고,

2) 단일 쓰레드로 구성되기 때문에 다른 쓰레드에 의해서 인터럽트에 걸릴 가능성도 존재하지 않는다 

33.5 문제점 - 블로킹 시스템 콜 Blocking System Call

쓰레드 기반 프로그래밍에서는 하나의 쓰레드가 I/O 를 대기할 때 다른 쓰레드를 실행하며 서버가 동작을 계속할 수 있다

I/O 처리와 다른 연산을 중첩 Overlap 시키면서 서버가 효율적으로 동작할 수 있다 

하지만 이벤트 기반 프로그래밍의 경우는 블로킹 시스템 콜을 호출해야 하는 이벤트가 존재할 때 문제가 발생한다 

쓰레드가 없고 단순히 이벤트 루프만 존재하기 때문에, 이벤트 핸들러가 블로킹 콜을 호출하면 서버 전체가 오직 그 일을 처리하기 위해 정지된다 (명령어가 끝날 때까지 다른 요청들의 처리가 중단된다)

이런 심각한 자원의 낭비가 발생하기 때문에, 이벤트 기반 시스템의 기본 원칙은 블로킹 호출을 허용하면 안 된다는 것이다 

33.6 해법 - 비동기 I/O 

이벤트 기반 서버의 한계를 극복하기 위해 디스크 입출력 요청 방식 중의 하나로 비동기 I/O, asynchronous I/O 방법을 개발하였다 

 

1) 이 인터페이스는 프로그램이 I/O 를 요청하면 I/O 요청을 완료하기 전에 리턴한다

2) MAC 상에서는 struct aiobc 또는 AIO 제어 블록, AIO Control Block 구조의 API 를 사용하는데 AIO 제어블럭을 적절히 설정하고,

3) 비동기 콜 (비동기 읽기 Asynchronous Read) 를 호출하면 읽기 작업의 완료를 대기하지 않고 즉시 리턴하면, 응용 프로그램은 하던 일을 계속 진행한다

4) aio_error() API 시스템 콜을 이용하여 해당 I/O 가 완료되었는지 계속적으로 검사 (폴링, polling) 하는 방식으로,

입출력 대기 시간 동안 더 많은 작업을 수행할 수 있게 된다 

🐣 비동기 I/O 는 프로그램이 디스크 입출력 I/O 을 요청할 때, 해당 작업이 완료될 때까지 기다리지 않고 즉시 다음 작업을 계속할 수 있도록 돕는 기술이다 

33.7 또 다른 문제점 - 상태 관리

이벤트 기반 접근법은 쓰레드 기반 코드보다 복잡하다, 왜냐하면 상태 관리가 필요하기 때문이다 

이벤트 기반 접근법에서는 이벤트 핸들러가 비동기 I/O 를 발생시킬 때, I/O 완료 시 사용할 프로그램 상태를 정리해 놓아야 한다

쓰레드 기반 프로그램에서는 쓰레드 스택에 그 정보들이 이미 들어 있기 때문에 불필요한 작업이다 

이벤트 기반 프로그래밍에서는 이것을 수동 스택 관리 Manual Stack Management 라고 부른다 

33.8 이벤트 사용의 어려움

이벤트 기반 접근법에는 또 다른 몇 가지의 어려움이 있다

1) 단일 멀티 CPU 에서는 이벤트 기반 접근법의 단순함이 사라진다 

단일 CPU에서 사용할 때는 락 없이, 다중 쓰레드의 인터럽트 없이 간단하게 처리했지만

다수의 CPU를 활용하기 위해서 다수의 이벤트 핸들러를 병렬적으로 실행하게 된다면

동기화 문제가 발생할 수 있고, 락이 없는 이벤트 처리 방식을 사용할 수 없다

2) 페이징 Paging 과 같은 종류의 시스템 작업과 조화롭게 실행되지 않을 수 있다 

이벤트 핸들러에서 페이지 폴트가 발생하면, 이벤트 서버는 블럭된다 

3) 소프트웨어가 개선되고 갱신되면서 루틴들의 특성이 변경될 수 있는데

이벤트 기반 서버에서는 변경되는 특성에 적합하게 코드를 다시 작성해야 한다, 이것이 쉽지 않다 

4) 비동기 디스크 I/O 가 일관성 있게 적용되어 있지 않다 

+ 정리하자면,

멀티코어 환경의 동기화 문제: 다수 CPU 사용 시 동기화 문제 발생 → 락 필요

운영체제와의 비호환성: 페이지 폴트 발생 시 이벤트 서버가 블록되어 성능 저하

루틴 특성 변화에 따른 수정 필요: 비차단/차단 특성 변화 시 코드 수정 필요 (+이벤트 핸들러도 수정해야 한대)

비동기 I/O의 일관성 문제: 네트워크와 디스크 I/O 간 비동기 처리 일관성 부족

33.9 요약 

이벤트 기반 서버는 스케줄링에 대한 제어권을 부여할 수 있다는 명확한 장점이 있지만 

복잡도가 높고 현대 시스템의 다른 부분들로 인해 적용이 어렵다는 문제가 있다

쓰레드 기반 접근법과 각자의 장점과 단점이 있지만, 시스템의 특성, 목적에 맞게 병렬 프로그램을 작성하면 된다 

CH34 병행성을 정리하는 대화

병행 알고리즘에 대한 초기 논문을 살펴보면 잘못된 것들이 가끔 있다 -> 병행성은 어렵다 

올바른 병행 코드를 작성하기 위해서는

1) 간단하게 만들어야 한다 

쓰레드 간의 복잡한 상호 동작을 피하고, 잘 알려지고 확실하게 검증된 기법을 사용하여 쓰레드들의 동작을 관리해야 한다

2) 병행성을 정말 필요할 때 사용하되 피할 수 있으면 피해야 한다 

어설프게 최적화된 프로그램처럼 나쁜 것은 없다 

3) 정말 병렬화가 필요하다면 단순화된 방식을 찾아봐야 한다 (ex. 병렬 데이터 분석 코드를 위한 맵리듀스 기법) 

4) 읽고 또 읽고, 작성하고 또 작성하고, 더 연습해라 

 

 

반응형