// 호출 스레드
make_donut(formed_D); // 아래 작업을 기다려줄 수 없을 때 -> 비동기 콜백함수를 사용하기
something_important(); // 기다려줄 수 없는 작업..
함수 내부에서 스레드를 생성하고 새로운 스레드가 콜백함수를 처리하도록 하기
void make_donut(func f){
//별도 생성된 스레드가 콜백함수를 처리
thread t(real_make_donut, f)
}
void real_make_donut(func f){
//..
f();
//..
}
주의할 점은 something_important()가 실행될 때 도넛 생성 작업은 아직 시작되지 않았을 수도 있다는 점
이것이 바로 비동기
이와같이 호출 스레드가 콜백 함수 실행에 의존하지 않는 것을 비동기 콜백이라고 함
request(handle); //handle 콜백함수를 비동기로 처리할 때
// -> request를 실행한 스레드와 handle을 실행한 스레드는 별개다
비동기 호출 프로그래밍 방식은 작업처리가 두 부분으로 나뉨
어떤 일을 해야하는지 알지만 언제 하게 될지 알 수 없음
request를 호출하고 handle을 넘겨주는 작업 (handle을 정의했지만 비동기로 실행되기 때문에)
언제 해야할지는 알지만 무엇을 해야하는지 모름
request 내부에서 처리되는 작업 (request 즉시 실행되지만 내부가 캡슐화 되었으므로)
콜백함수의 정의
다른 코드에 매개변수로 전달되는 실행 가능한 코드
콜백함수와 주 프로그램은 같은 계층에 있지만 해당 콜백함수를 작성만하지 직접 호출하지 않는다는 것
콜백함수가 호출되는 시점
특정 이벤트가 발생하고 이를 처리할 수 있는 코드를 호출할 때 콜백함수가 유용
이 관점에서 콜백함수는 이벤트 핸들러
콜백함수로 전달되는 이벤트 핸들러는 보통 비동기 콜백임
동기 콜백이면 이벤트 발행될 때까지 다른 작업 불가.. (while을 돌며 이벤트 발행여부를 확인 하고 있으므로..)
동기 콜백 vs 비동기 콜백
비동기 콜백은 다중 코어 리소스를 더 잘 활용가능
비동기 콜백은 파일 입출력 작업, 웹서비스처럼 동시성이 높은 시나리오에 적합
비동기 콜백의 문제
콜백 지옥
비즈니스가 복잡한 경우 콜백 지옥에 빠질 가능성이 높음
비동기 콜백의 효율과 동기 콜백의 단순성을 함께 누리는 방법이 코루틴
동기 vs 비동기
동기 호출은 보통 같은 스레드 내에서 진행
파일 입출력은 동기지만 다른 스레드에서 진행됨
read()를 호출하여 파일을 읽는 상황이라면 시스템 콜을 운영체제에 보내고 운영체제는 호출 스레드를 일시 중지 시키고 커널이 디스크 내용을 읽어오면 호출 스레드가 재시작됨
블로킹 입출력
이처럼 스레드가 여러개여도 동기일 수 있음
비동기 호출은 보통 시간이 많이 걸리는 입출력 작업을 백그라운드 형태로 실행함
파일 읽고 쓰기, 네트워크 데이터 송수신, 데이터베이스 작업 등
비동기 호출 방식에서 작업이 실제로 완료되는 시점을 어떻게 파악할 수 있을까??, 그 결과를 어떻게 처리해야할까??
두 가지 상황이 존재
호출자가 실행결과를 전혀 신경쓰지 않을 때
호출자가 실행결과를 반드시 알아야 할 때
알림 작동 방식
웹서버에서 동기와 비동기 작업
웹서버 요청 처리 작업 중 대표적인 것이 데이터베이스 요청
예시 상황
DB 외 입출력 없는 상황
A,B,C 작업
데이터베이스 요청
D,E,F 작업
일반적으로 이런 형태의 웹서버에는 주 스레드와 데이터베이스 처리 스레드가 존재
동기 처리 시 주 스레드의 유휴 시간이 발생
비동기 구현에선 주 스레드가 데이터베이스 요청 전송 후 바로 새로운 사용자 요청을 직접 처리함
주스레드가 A,B,C처리 후 데이터베이스 요청 후 새로운 사용자 요청을 받는다면 D,E,F는??
두가지 상황 존재
주 스레드가 데이터베이스 처리 결과를 신경쓰지 않음
주 스레드가 아닌 데이터베이스 스레드가 데이터베이스 처리 후 이어서 D,E,F처리
데이터베이스 스레드가 D,E,F 작업에 대해 어떻게 아는지?
주 스레드에서 D,E,F를 콜백함수로 넘긴다 (데이터 베이스 호출과 함께)
주 스레드가 데이터베이스 처리 결과에 관심을 가질 때
데이터베이스 스레드는 알림작동 방식을 이용하여 작업 결과를 주 스레드로 전송
주스레드는 메시지를 수신하면 후반부작업 처리
데이터베이스 스레드가 유휴상태 (입출력 기간동안) 라는점을 제외하면 주스레드에 유휴시간은 없음
데이터베이스 스레드가 후반부작업을 처리하는 것보다는 비효율적이지만 동기보다는 효율적
블로킹과 논블로킹
프로그래밍에서 함수를 호출할 때 주로 사용
함수A가 함수B를 호출할 때
B를 호출함과 동시에 운영체제가 함수A가 실행 중인 스레드나 프로세스를 일시 중지 시킨다면 블록킹
아니면 논블로킹
함수 호출로 인해 호출자의 스레드나 프로세스가 운영체제에 의해 일시중지 되는 것은 어떤 경우??
블로킹의 핵심 문제 : 입출력 - 일반적으로 입출력 시 호출 스레드가 블로킹 됨
디스크 예시
cf) 디스크 입출력 행위는 cpu가 처리하지 않음
cpu는 입출력 요청을 디스크 컨트롤러에 전달할뿐임
실데 디스크 입출력 작업(데이터 읽고 메모리로 전송)은 전용 하드웨어가 처리함
디스크가 하나의 트랙 탐색 입출력 요청을 완료하는데 소요되는 시간은 ms단위 수준
우리 스레드에서 입출력 과정이 실행되는 동안 cpu 제어권을 다른 스레드에 넘겨 작업 (블로킹)할수 있도록 함
완료되면 cpu 제어권을 우리 스레드 혹은 프로세스에서 넘겨받아 다음 작업을 실행함
cf) 스레드는 I/O 요청을 기다리는 동안 블로킹 상태로 있게 되지만, 시스템은 다른 스레드가 CPU를 활용할 수 있도록 CPU 제어권을 넘기기 때문에 스레드 차원에서의 블로킹은 맞고, 시스템 차원에서 다른 스레드가 CPU를 사용할 수 있도록 한다는 점에서는 논블로킹처럼 보이기도 함. 하지만 개념적으로 스레드가 기다린다는 측면에서 "블로킹"이 맞음
cpu제어권을 상실했다가 되찾는 시간 동안 스레드나 프로세스는 블로킹되어 일시 중지됨 (스레드 양도)
시간이 많이 걸리는 입출력 작업이 포함될 때 가끔 호출 스레드가 블로킹되어 일시 중지되는 일이 발생람
호출 스레드가 블로킹되지 않고 입출력 작업처리하는 방법 : 논블로킹 사용
논블로킹과 비동기 입출력 - 호출 후 함수 즉시 반환
네트워크 데이터 수신 예시
네트워크 수신 함수 recv가 논블로킹이면 이 함수를 호출할 때 운영체제는 호출 스레드를 일시 중지시키는 대신 recv 함수 즉시 반환
호출 스레드는 자신의 작업을 계속하며 데이터 수신 작업은 커널이 진행
네트워크 데이터 언제 수신했는지 어떻게 알 수 있을까?? : 세가지 방법
결과 확인 함수 제공
알림 작동 방식
수신처리 콜백함수를 recv인자로 전달
이것이 논블로킹 호출
이런 유형의 입출력 작업이 비동기 입출력
동기와 블로킹, 비동기와 논블로킹
동기와 블로킹
동기 호출이 항상 블로킹인 것은 아님
블로킹은 모두 동기 호출임
funcA함수가 sum함수를 호출했다고 블로킹되거나 스레드가 일시중지 되지는 않음
비동기와 논블로킹
recv함수에 비동기 콜백함수 전달한 것은 논블로킹이면서 비동기
recv함수 호출 후 check함수로 계속해서 결과 감지한다면 논블로킹이지만 동기
높은 동시성과 고성능을 갖춘 서버 구현
다중 프로세스
가장 먼저 출현한 기술
간단한 형태의 병행처리방식 일종인 다중 프로세스
프로세스간 통신에 난이도
프로세스 생성 종료 성능 부담
다중 스레드
프로세스 주소 공간 공유하기 때문에 리소스 공유에 별도 통신 필요 없음
스레드 생성 종료 부담 적음
각 요청에 대응하는 스레드 생성 가능
프로세스 주소공간 공유에 따른 단점 존재
주소공간 공유하기 때문에 하나의 스레드 강제 종료 시 같은 프로세스 공유하는 모든 스레드와 프로세스 강제 종료됨
더 나은 작동 방식은 운영체제에 ‘소켓 디스크립터 열개 감시하고 있다가 데이터가 들어오면 알려줘’ 라는 내용 전달하는 작동 방식 사용
이런 작동 방식을 입출력 다중화라함
입출력 다중화는 이벤트 순환 엔진이 된다
리눅스에서 유명한 것이 epoll
// 이벤트 순환을 위한 epoll 생성
epoll_fd = epoll_create(); //getEvent 동일
// 서술자를 epoll이 처리하도록 지정
Epoll_ctl(epoll_fd, fd1, fd2, fd3, fd4...);
while (1)
{
int n = epoll_wait(epoll_fd);
for (i = 0; i < n; i++)
{
// 특정 이벤트 처리
}
}
두번째 문제 : 이벤트 순환과 다중 이벤트
이벤트 순환과 이벤트 핸들러 단일 스레드 예시
이벤트 핸들러에서
입출력 작업이 없고
처리시간이 짧을 때
이러한 상황은 이벤트 순환과 이벤트 핸들러가 같은 스레드에서 실행되도 문제 없음
하지만 사용자 요청을 처리하는데 cpu가 많은 시간을 소모한다면??
요청A를 처리하는 중에 요청 B를 처리할 수 없음
문제2 해결 방법 - 다중 스레드 (반응자 패턴)
이벤트 순환과 이벤트 핸들러가 다른 스레드
요청 처리속도를 높이고 다중 코어 최대한 활용
이벤트 순환은 요청을 수신하면 간단한 처리 후 바로 각각의 작업자 스레드에 분배
작업자 스레드를 스레드 풀로 관리 가능
이 설계 방법은 반응자 패턴이라는 이름
이벤트 순환과 입출력
요청 처리 과정에서 입출력도 존재하는 경우에서 두가지 상황
1. 입출력 작업에 대응하는 논블로킹 인터페이스가 있는 경우
논블로킹 인터페이스를 호출해도 스레드가 일시중지되지 않으며 인터페이스가 즉시 반환되므로 이벤트 순환에서 직접 호출이 가능
2. 입출력 작업에 블로킹 인터페이스만 있는 경우 - 이벤트 순환 내부에서 블록킹 호출하면 안됨
이벤트 순환내에서 절대로 블로킹 인터페이스를 호출해선 안됨
이벤트 순환 스레드가 일시중지 될 수 있음
블로킹 입출력 호출이 포함된 작업은 작업자 스레드에 전달해야함!
비동기와 콜백함수
예시) 서버는 일반적으로 원격 프로시저 호출, 즉 RPC를 통해 통신함
RPC는 네트워크 설정, 데이터 전송, 데이터 분석 등 지루한 작업을 담아 프로그래머가 일반함수를 호출하는 것처럼 네트워크로 통신할 수 있도록 처리함
void handler(request) {
A;
B;
GetUserInfo(request, response); // 서버 A에 요청
C;
D;
GetQueryInfo(request, response); // 서버 B에 요청
E;
F;
GetStorkInfo(request, response); // 서버 C에 요청
G;
H;
}
Get으로 시작하는 RPC호출은 모두 블로킹 호출이기 때문에 사용자가 응답하기 전에는 함수가 반환되지 않음
블로킹 호출이기 때문에 스레드가 일시중지 될 수 있고 블로킹 호출이 여러번 발생하면 스레드 빈번하게 중단되어 cpu 비효율적임