Skip to main content

Command Palette

Search for a command to run...

비동기 프로그래밍

Updated
3 min read
비동기 프로그래밍

비동기 프로그래밍이란?

비동기 프로그래밍이란 무엇인가? 작성한 순서대로 작동하는 프로그래밍 모델을 동기 프로그래밍(synchronous programming) 이라고 부른다.

반대로 비동기 프로그래밍은 작성한 순서대로만 작동하는 것은 아님을 의미하게 되겠다. 정확히는 독립해서 발생하는 이벤트에 대한 처리를 기술하기 위한 동시성 프로그래밍 기법을 총칭해서 비동기 프로그래밍이라고 한다. 비동기 프로그래밍을 이용하면 전화가 울리면 전화를 받는 것과 같이 이벤트에 대응한 작동을 구현할 수 있다.

비동기 프로그램을 구현하는 방법으로 콜백 함수나 시그널(인터럽트)을 이용하는 방법이 있으나 여기서는 Future, async/await를 설명한다. 러스트에서는 async/await을 이용한 비동기 라이브러리로 Tokio, smol이 있다. 또한 nix와 future크레이트를 이용할 것이다. smol github

동시 서버

여기서는 반복 서버, 동시 서버를 알아보고 그 구현을 설명한다.

반복 서버(interactive server): 클라이언트로 요청받은 순서대로 처리하는 서버 동시 서버(concurrent server): 요청을 동시에 처리하는 서버이다.

반복 서버

다음은 단순한 반복 서버의 예시 코드다.

use ::std::io::{BufRead, BufReader, BufWriter, Read};
use std::net::TcpListener;

fn main() {
    // TCP 8000번 포트 리스닝
    let listener = TcpListener::bind("127.0.0.1:8000").unwrap();
    // 커넥션 요청을 받아들이기
    while let Ok((stream, _)) = listener.accept() {
        // 읽기, 쓰기 객체 생성
        let stream0 = stream.try_clone().unwrap();
        let mut reader = BufReader::new(stream0);
        let mut writer = BufWriter::new(stream);

        // 클라이언트로부터 한 줄 읽기
        let mut buf = String::new();
        reader.read_line(&mut buf).unwrap();
        writer.write_all(buf.as_bytes()).unwrap();
        writer.flush().unwrap();
    }
}

위 코드에서는 커넥션 요청을 받아 클라이언트로부터 데이터를 수신하고 송신 처리를 완료하지 않으면 다음 클라이언트의 처리를 수행할 수 없다.

동시 서버

동시 서버는 클라이언트로부터의 커넥션 요청, 데이터 도착 등의 처리를 이벤트 단위로 세세히 분류하여 이벤트에 따라 처리할 수 있다. 네트워크 소켓이나 파일 등 IO이벤트 감시에는 리눅스에서 epoll BSD 계열에서는 kqueue라는 시스템 콜을 이용할 수 있다.

IO 이벤트 감시는 파일 디스크립터를 감시하는 것이다. 여러 TCP 커넥션이 존재할 때 서버는 여러 개의 파일 디스크립터를 가지는데, 이 디스크립터들에 대해 읽기 쓰기 가능 여부를 epoll 등을 이용해 판정할 수 있다.

그림에서는 프로세스 A가 0~4까지의 파일 디스크립터를 이용하는데, 커널 내부의 프로세스와 디스크립터 정보가 저장되어 있으므로 이 정보들을 이용해 epoll등을 통한 파일 디스크립터 감시를 수행한다.

다음 코드는 epoll을 이용한 병렬 서버의 예시 코드이다. 다만 논블로킹 설정은 수행되지 않았다.

use nix::sys::epoll::{
    EpollCreateFlags, EpollEvent, EpollFlags, EpollOp, epoll_create1, epoll_ctl, epoll_wait,
};
use std::collections::HashMap;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::net::TcpListener;
use std::os::unix::io::{AsRawFd, RawFd};

fn main() {
    // epoll을 이용해 여러 연결을 동시에 감시하기 위해 준비
    let epoll_in = EpollFlags::EPOLLIN;
    let epoll_add = EpollOp::EpollCtlAdd;
    let epoll_del = EpollOp::EpollCtlDel;
    // 서버 소켓 생성
    let listener = TcpListener::bind("127.0.0.1:8081").unwrap();
    // epoll 인스턴스 생성
    let epfd = epoll_create1(EpollCreateFlags::empty()).unwrap();

    let listen_fd = listener.as_raw_fd();
    // 서버 소켓을 epoll에 등록: 새로운 연결이 들어올 때 알려달라고 커널에 요청
    let mut ev = EpollEvent::new(epoll_in, listen_fd as u64);
    epoll_ctl(epfd, epoll_add, listen_fd, &mut ev).unwrap();

    // 동시성: 클라이언트별 버퍼를 HashMap에 저장
    let mut fd2buf = HashMap::new();
    let mut events = vec![EpollEvent::empty(); 1024];

    // 단일 스레드에서 다수 연결을 epoll_wait로 동시에 처리
    while let Ok(nfds) = epoll_wait(epfd, &mut events, -1) {
        for n in 0..nfds {
            if events[n].data() == listen_fd as u64 {
                // 새 클라이언트 접속
                if let Ok((stream, _)) = listener.accept() {
                    let fd = stream.as_raw_fd();
                    // 각각의 클라이언트 연결을 독립적으로 관리
                    let stream0 = stream.try_clone().unwrap();
                    let reader = BufReader::new(stream0);
                    let writer = BufWriter::new(stream);
                    fd2buf.insert(fd, (reader, writer));
                    println!("accept: fd = {}", fd);

                    // 새 클라이언트 소켓을 epoll에 등록: 이후부터 이 소켓 읽기 이벤트 감시
                    let mut ev = EpollEvent::new(epoll_in, fd as u64);
                    epoll_ctl(epfd, epoll_add, fd, &mut ev).unwrap();
                }
            } else {
                // 기존 클라이언트로부터 데이터 도착
                let fd = events[n].data() as RawFd;
                let (reader, writer) = fd2buf.get_mut(&fd).unwrap();

                let mut buf = String::new();
                let n = reader.read_line(&mut buf).unwrap();

                if n == 0 {
                    // 클라이언트가 연결을 종료한 경우
                    let _ev = EpollEvent::new(EpollFlags::empty(), fd as u64);
                    epoll_ctl(epfd, epoll_del, fd, None).unwrap();
                    fd2buf.remove(&fd);
                    println!("close: fd = {}", fd);
                    continue;
                }
                println!("read: fd = {}, buf = {}", fd, buf.trim());
                // 읽은 데이터를 다시 클라이언트로 전송
                writer.write_all(buf.as_bytes()).unwrap();
                writer.flush().unwrap();
            }
        }
    }
}

이렇게 epoll 등을 사용해 여러 IO에 대해 동시에 처리를 수행하는 방법을 IO 다중화(IO multiplexing) 이라고 부른다. IO다중화의 방법론의 하나로 위 코드에서처럼 이벤트에 대해 처리를 기술하는 프로그래밍 모델, 디자인 패턴을 이벤트 주도(event-driven) 이라고 한다.

29 views

More from this blog

락프리 데이터 구조와 알고리즘

여기서는 락프리 데이터 구조를 설명한다. 락프리(lock-free) 란 배타락을 이용하지 않고 처리를 수행하는 데이터 구조 및 그에 대한 조작 알고리즘을 총칭한다. 왜 락프리인가? 전통적인 동시성 제어 방법인 뮤텍스나 세마포어는 여러 문제점을 가지고 있다: 성능 저하: 락 경합(lock contention)으로 인한 대기 시간 데드락: 여러 스레드가 서로의 락을 기다리는 상황 우선순위 역전: 낮은 우선순위 스레드가 높은 우선순위 스레드를 ...

Jul 27, 20257 min read126

소프트웨어 트랜잭셔널 메모리

소프트웨어 트랜잭셔널 메모리 동시성 프로그래밍에서 공유 자원에 대한 안전한 접근은 항상 중요한 과제다. 전통적으로 뮤텍스 락과 같은 비관적 락(Negative Lock) 방식을 사용해왔다. 이 방식은 크리티컬 섹션에 진입하기 전에 반드시 락을 획득해야 하며, 락을 얻지 못하면 코드 실행 자체가 블록된다. 하지만 이와는 다른 접근 방식이 있다. 바로 낙관적 락(Optimistic Lock) 방식인데, 이는 "일단 실행하고 나중에 검증하자"는 철학...

Jul 20, 202517 min read263

공평한 배타 제어

공평한 배타 제어 여기서는 공평한 배타 제어에 대해 설명한다. 먼저 컨텐션(contention) 이라는 개념을 이해할 필요가 있다. 컨텐션이란 여러 스레드가 동시에 같은 락을 획득하려고 경쟁하는 상황을 말한다. 컨텐션이 높을수록 스레드들이 락을 기다리는 시간이 길어지고 성능이 저하된다. 이러한 컨텐션 상황은 시스템 아키텍처에 따라 더욱 복잡해질 수 있다. 특히 비균일 메모리 접근(Non-Uniform Memory Access, NUMA) 와 같...

Jul 13, 20259 min read21

KernelSnitch[논문 리뷰]

Paper 1. Intro 이 글은 NDSS 2025에서 발표된 KernelSnitch 논문을 소개이다. 이 연구는 커널의 평범한 데이터 구조체들이 가진 본질적인 특성이 어떻게 심각한 보안 취약점이 되는지를 보여준다. 핵심은 이러하다: "데이터 구조체의 크기에 따른 접근 시간 차이를 이용해 커널의 비밀 정보를 유출할 수 있다" 여기서는 커널 힙 포인터 유출에 집중해서 설명한다. 이 공격이 성공하면 KASLR을 우회하고 더 심각한 커널 익스플로...

Jul 11, 20257 min read131

멀티태스크와 액터 모델

멀티태스크 협조적/비협조적 멀티태스크 선점: 프로세스와의 협조 없이 수행하는 컨택스트 스위칭이라고는 하나, 결국 뺏어오는 게 가능하냐의 문제다. 협조적 멀티태스크(비선점형, cooperative): 각각의 프로세스가 자발적으로 컨택스트 스위칭을 수행하는 멀티태스크 방식. 장점: 멀티태스크 매커니즘을 구현하기 쉽다. 단점: 프로세스가 자발적으로 컨텍스트 스위칭을 해야하는데, 만약 버그가 발생하여 프로세스가 무한 루프에 빠지거나 정지하게 되면 그 ...

Jul 6, 20252 min read25
M

MaxLog

35 posts