# 1장 \~ 3장

## 1장 멀티스레딩

### 1.1 프로그램과 프로세스

프로그램 : 코드 + 데이터 프로세스

* 프로그램 실행하면 그 안의 명령어 한 줄씩 실행되면서 활동하는 상태
* 코드, 데이터, 힙, 스택 영역

### 1.2 스레드

스레드 : 프로세스처럼 명령어 한 줄씩 실행하는 기본 단위

* 프로세스 안에 스레드 여러 개
* 같은 프로세스면 스레드끼리 메모리 공간 공유
* 스레드마다 스택 가짐. 함수 로컬 변수는 스레드마다 있다는 의미.
* 스레드 간의 실행 순서는 기본적으로 랜덤
* 자식 스레드보다 메인 스레드가 먼저 종료되면 자식은 좀비 됨

호출 스택

* 프로그램은 서브루틴 혹은 함수로 구성됨
* 함수는 실행 끝나면 호출됐던 지점으로 되돌아가야 함
* 그 정보가 저장된 곳을 call stack(호출 스택)이라 함
* 호출 스택 안에는 각 함수 지역 변수도 들어있음
* 함수, 인자, 메모리까지 다 동일해도 실행 지점은 약간 차이 생길수도 있다
* 따라서 각 스레드는 각자의 호출 스택을 가진다

`std::thread()` : 모던 c++의 쓰레드 생성 (운영체제 통일)

### 1.3 멀티스레드 프로그래밍은 언제 해야 할까?

대표적 상황

* 오래 걸리는 일 하나와 빨리 끝나는 일 여럿을 같이 해야 할 때
* 어떤 긴 처리를 진행하는 동안 다른 짧은 일을 처리해야 할 때
  * 디스크 처리 결과 기다릴 때 서버 CPU 놀지 않게 다른 플레이어에게 배분
* 기기에 있는 CPU를 모두 활용해야 할 때
  * 기본적으로 스레드는 코어를 1개만 씀

### 1.4 스레드 정체

컨텍스트 스위치

* 컴퓨터는 여러 프로세스와 그 안의 스레드를 일정 시간마다 번갈아가며 실행
* 연산량 많음 : 스레드 상태(call stack 등)을 저장 → 과거 실행하던 스레드 중 선택 → 스레드 복원 → 실행 지점 강제 이동
* 사람이 쾌적하게 느낄만한 타임 슬라이스. 보통 0.5밀리초 안에 스레드 하나 전환.
* Runnable 스레드 개수가 CPU 개수 이하면 컨텍스트 스위칭 발생 이유 없음
* 컨텍스트 스위치는 기계어 명령어 단위로 일어남. 한 줄 구문 안에 있는 거 실행하다 컨텍스트 스위칭 가능.

### 1.5 스레드를 다룰 때 주의 사항

경쟁 상태(data race) : 두 스레드가 같은 데이터에 접근

* 컨텍스트 스위치는 기계어 단위로 자르기 때문에, `+=`여도 할당 하기 전에 다른 스레드가 건드릴 수 있음
* `Array<int>`에 넣는 거 경쟁 상태 일어나면, 이상한 값이 넣어지는게 아니라 아예 crash 일어남
  * 배열 객체에 더 이상 넣을 공간 없으면 메모리 재할당
  * C 언어 런타임 시 메모리 재할당 후 메모리 주소 달라지는 경우 있다

### 1.6 임계 영역과 뮤텍스

mutex : 사용권 내놓기 전에 못 사용

```cpp
std::mutex mx;
mx.lock();
write(x);
mx.,unlock();
```

* 이래버리면, write() 실행하다 exception 발생 시 unlock() 실행 안 됨.
* c++에선 로컬 변수 파괴될 때, 파괴자 함수에서 원하는 코드 실행 가능
  * 이를 이용하면 exception 발생해도 파괴자 함수로 unlock() 자동 실행

```cpp
std::recursive_mutex mx;
lock_gaurd<recursive_mutex> lock(mx);
read(x);
```

* `lock_guard()` : 뮤텍스 잠금 상태를 로컬 변수로 저장하고, 사라질 때 자동 잠금 해제
* `recursive_mutex` : 같은 스레드에서 lock 중복 잠금 가능

```cpp
object mx = new object();
lock(mx)
{
	// 코드
}
```

* 별도 unlock()호출 안 해도 lock 구문 블록 나갈 때 자동 잠금 해제
* `lock(obj)` : obj 참조값 기준으로 소프트웨어 락. 커널 수준 아님.

```cpp
    // 작동할 워커 스레드
    vector<shared_ptr<thread>> threads;

    for (int i = 0; i < 4; i++)
    {
        shared_ptr<thread> t(new thread([&](){
            // 각 스레드의 메인 함수
            // 값을 가져올 수 있으면 루프를 돈다
            while(true)
            {
                int n;
                {
                    lock_guard<recursive_mutex> num_lock(num_mutex);
                    n = num;
                    num++;
                }
                if(n >= MaxCount)
                    break;

                if(IsPrimeNumber(n))
                {
                    lock_guard<recursive_mutex> primes_lock(primes_mutex);
                    primes.push_back(n);
                }
            } 
        }));
        // 스레드 객체를 일단 갖고 있는다
        threads.push_back(t);
    }
```

* 이렇게 스레드를 4개로 나눠서 작업해도, 정확히 4배 빨라지진 않는다.
  * lock 때문에 다른 스레드가 대기할 수도 있음 (락 풀릴 때까지 별 거 안해서 성능 영향은 미미)
  * 메모리에 여러 CPU 접근 시에 블로킹 발생. (메모리 접근 시간을 메모리 바운드 시간이라 함)

```cpp
class Player
{
	// 각 변수에 대한 락 선언
	CriticalSection m_positionCritSec;
	Vector3 m_position; // 1
	CriticalSection m_nameCritSec;
	string m_name; // 2
	CriticalSection m_hpCritSec;
	int m_hp; // 3
}
```

* 뮤텍스를 잘게 나누면 문제점
  * 뮤텍스 액세스 과정 무거워서 성능 떨어짐
  * 교착상태 쉽게 발생함 (가장 심각)

위와 같은 코드에서, 변수들을 무조건 1,2,3 순서대로 액세스한다는 규칙을 지키면서 코딩해야 교착 상태 피한다.

### 1.7 교착 상태

교착 상태 : 두 스레드가 서로를 기다리는 상황

서버에서 교착 상태 되면 발생하는 현상

* CPU 사용량이 현저히 낮거나 0%
* 클라가 서버를 이용 불가

`CRITICAL_SECTION` 사용 방법

* window에서만 가능, include windows.h 해야 함.
* `InitializeCriticalSectionEx()` : 임계 영역 생성
* `DeleteCriticalSection()` : 임계 영역 제거
* `EnterCriticalSection()` : 임계 영역 잠금
* `LeaveCriticalSection()` : 임계 영역 잠금 해제

```cpp
// CRITICAL_SECTION wrapper
// enter, leave 어쩌구를 lock, unlock으로 바꾼다
class CriticalSection
{
	CRITICAL_SECTION m_critSec;
	public:
	CriticalSection();
	~CriticalSection();

	void Lock();
	void Unlock();
};

// CriticalSection wrapper
// lock_guard처럼 만들기 위해, 소멸자에 unlock() 호출
class CriticalSectionLock
{
	CriticalSection* m_pCritSec;
	public:
	CriticalSectionLock(CriticalSection& critSec);
	~CriticalSectionLock();
};

CriticalSection::CriticalSection() { InitializeCriticalSectionEx(&m_critSec, 0, 0); }
CriticalSection::~CriticalSection() { DeleteCriticalSection(&m_critSec); }

void CriticalSection::Lock() { EnterCriticalSection(&m_critSec); }
void CriticalSection::Unlock() { LeaveCriticalSection(&m_critSec); }

CriticalSectionLock::CriticalSectionLock(CriticalSection& critSec)
{
	m_pCritSec = &critSec;
	m_pCritSec->Lock();
}

CriticalSectionLock::~CriticalSectionLock() { m_pCritSec->Unlock(); }
```

편하게 쓰기 위해 만든 wrapper 클래스

```cpp
int a;
CriticalSection a_mutex;
int b;
CriticalSection b_mutex;

int main()
{
	// t1 스레드를 시작한다
	thread t1([]()
	{
		while (1)
		{
			CriticalSectionLock lock(a_mutex);
			a++;
			CriticalSectionLock lock2(b_mutex);
			b++;
			cout << "t1 done.\n";
		}
	});

	// t1 스레드를 시작한다
	thread t2([]()
	{
		while (1)
		{
			CriticalSectionLock lock(b_mutex);
			b++;
			CriticalSectionLock lock2(a_mutex);
			a++;
			cout << "t2 done. \n";
		}
	});

	// 무한 반복 돌렸으니 여기까진 안 옴
	t1.join();
	t2.join();
}
```

인위적으로 교착 상태 일으키는 코드

* 디버그 일시정지 후 디버그 > 창 > 스레드에서 콜 스택 확인 가능.
* mutex의 OwningThread를 확인하고 10진수로 바꿔보면 스레드의 ID

### 1.8 잠금 순서의 규칙

뮤텍스 여러 개 사용할 때 교착 상태 예방하려면, 잠금 순서를 그래프로 그려두어야 한다. 잠금을 할 때 그래프 보면서 순서 맞는지 확인.

이미 A → B → C 순서대로 잠가놨는데, 다른 곳에서 C → B 순서로 잠그면 교착 상태 일어난다.

재귀 뮤텍스 : lock() 중첩 가능. 여러 번 잠그면 여러 번 풀어야 함. 이미 잠근 후엔 순서 다르게 추가로 잠가도 상관 없음

### 1.9 병렬성과 시리얼 병목

병렬성(parallelism) : 여러 CPU가 각 스레드 연산 실행하여 동시 처리량 올림 시리얼 병목(serial bottleneck) : 병렬 실행되게 만들었는데 한 CPU만 연산 수행 암달의 법칙 : 연산 50%를 아무리 최적화해도 최대 2배 개선

Concurrency Visualize : 멀티스레드 프로그램 병목 있는지 분석 및 시각화 가능한 Visual Studio Extension

잠금 후에 디스크 읽는 코드 같은 거 넣으면 성능 떡락 그때만이라도 락을 풀던가 해라.

### 1.10 싱글 스레드 게임 서버

싱글 스레드 서버

* 멀티 스레드 어려우니까 그냥 싱글 스레드로 하기도 한다
* CPU 개수만큼 프로세스를 띄운다.
* 디스크 로딩 시 시리얼 병목. 해결 위해 비동기 함수나 코루틴 사용.
* 부득이한 경우 아니면 방 개수만큼 스레드나 프로세스 띄우는 건 피하기.
  * 컨텍스트 스위칭이 마구 일어나기 때문에, 실제 처리 동접자 수 떡락

### 1.11 멀티 스레드 게임 서버

멀티 스레드 서버

* 서버 프로세스 많이 띄우기 곤란할 때 (MMO같이 게임 용량 크거나 연산 많음)
* 코루틴이나 비동기 쓸 수 없고 디바이스 타임 발생
* 서버 인스턴스를 서버 기기당 하나만 둠
* 서로 다른 방이 같은 메모리 공간 액세스해야 함
* 공통 데이터 뮤텍스, 방 별 뮤텍스 따로

### 1.12 스레드 풀링

* 클라에 일일이 스레드 할당하지 않고 스레드 풀에서 꺼내쓴다
* CPU 코어 수만큼 스레드 만들어도 스레드 쉬는 동안 CPU도 쉼.
  * 디바이스 타임 발생하면 스레드가 CPU보다 많아야 함

### 1.13 이벤트

이벤트 : 잠자는 스레드를 깨우는 도구

* 상태 값 : Reset(이벤트 없음), Set(이벤트 있음)

```cs
Event event1;

void Thread1() { event1.Wait(); } // 이벤트 신호를 기다린다
void Thread2() { event1.SetEvent(); } // 이벤트에 신호한다
```

자동 이벤트 : 스레드 깨운 뒤에 상태 값 0 수동 이벤트 : 깨워도 1로 남음

### 1.14 세마포어

```cs
Semaphore sema1;

void Main() { sema1 = Semaphore(2); }

void Thread1()
{
	sema1.Wait();
	sema1.Release();
}
void Thread2()
{
	sema1.Wait();
	sema1.Release();
}
void Thread3()
{
	sema1.Wait();
	sema1.Release();
}
```

### 1.15 원자 조작

atmoic operation : 뮤텍스나 임계 영역 잠금 없이도 여러 스레드가 안전하게 접근 가능

* 원자성을 가진 값 더하기
* 원자성을 가진 값 맞바꾸기
* 원자성을 가진 값 조건부 맞바꾸기

### 1.16 멀티스레드 프로그래밍의 흔한 실수들

멀티스레드 버그는 발생 빈도도 랜덤이고, 버그 원인과 발생지가 다를 때 많음.

1. 읽기와 쓰기 모두에 잠금하지 않기

```cs
void func1()
{
	// lock(a_mutex) 누락
	print(a);
}
void func2()
{
	lock(a_mutex)
	a = a+10;
}
```

읽기에 락 안 걸면 가끔 비정상적 값 출력

2. 잠금 순서 꼬임

프로그램 규모 커지면 잠금 순서 지키기 어려움. 최선은 규칙을 최대한 적게 유지하는 것.

3. 너무 좁은 잠금 범위

잠금 범위 너무 넓으면

* 처리 병렬성 떨어짐
* 스레드 대기가 길어져서 컨텍스트 스위치 많아짐

잠금 범위 너무 좁으면

* 컨텍스트 스위치 확률 떨어지지만,
* 임계 영역 잠금은 단순 산술 연산보다는 처리 시간 많음
  * 임계 영역 잠금은 원자 조작. 원자 조작은 일반 메모리 접근보다 시간 더 걸림.
* 보호 합칠 수 있으면 합치는게 좋다

4. 디바이스 타임이 섞인 잠금

로그 출력이나 콘솔 함수 (printf나 cout) 는 디바이스 타임 발생 디버깅 목적으로 콘솔 출력 뿌리는데 메모리에 잠금하면 병목 생김

5. 잠금의 전염성으로 발생한 실수

```cs
void func()
{
	lock(list_mutex);
	A* a = list.GetFirst();
	unlock(list_mutex);
	
	a->x++; // 문제가 되는 부분
}
```

잠금 전염성 : 잠금으로 보호된 리소스에서 얻은 값이나 포인터가 로컬 변수에 있어도 잠금 유지해야 함

합성 가능성의 부족 (lack of composability) : 여러 모듈이나 로직을 쉽게 조립할 수 없는 것. 사용자 A 변수 차감 → 사용자 B 변수 증가인 프로그램 있을 때, 사용자 A도 B도 뮤텍스로 잠가놨어도 송금 프로그램이 둘 이상 스레드 실행할 때, 오작동 가능. (A만 깎고 B 증가 못한다던지)

6. 잠금 뮤텍스나 임계 영역 삭제

잠금 중인 뮤텍스 삭제하는 실수 종종 한다. 파괴자 함수에 "이미 잠금돼있으면 오류 발생" 넣으면 감지 가능

## 2장 컴퓨터 네트워크

### 2.1 컴퓨터 네트워크를 구성하는 기기

#### OSI 모델

계층 2

* 각 단말기는 고유한 주소를 갖는다
* 단말기는 데이터를 프레임(헤더+페이로드)이라는 단위로 주고받는다

계층 3

* 서로 다른 LAN들이 연결된 WAN에서 필요한 규약
* 라우터 : 데이터 받았는데 스위치가 직접 건내줄 수 없으면 취급할 수 있는 곳으로 보낸다
* 단말기 A에서 단말기 B로 보낼 때 데이터는 IP 패킷 형태로 포장되어 전송

### 2.2 인터넷

OSI 계층 3 IP 규약만 지키면 기기 종류 상관없이 소통 가능 (랜섬, 광섬유, 무선, 전화선...) 서로 다른 종류의 많은 스위치와 라우터가 연결되어 지구를 뒤덮은게 인터넷

### 2.3 컴퓨터 네트워크 데이터

프레임 : OSI 계층 2의 데이터 단위 패킷 : OSI 계층 3의 데이터 단위 서버 앱 개발에는 스트림과 메시지를 다룬다

스트림 형식

* 스트림 : 한쪽에서 다른 한쪽으로 연결된 데이터 흐름 하나
* 보낸거랑 받은거랑 횟수나 데이터 순서 일치 안 할수도 있음
* 헤더 방식 : 보낼 데이터 크기 먼저 보내기
* 구분자(delimiter) 방식 : 데이터 시작이나 끝을 알리는 기호 추가

메시지 형식

* 자체적으로 데이터 시작과 끝 구별 가능
* 14~~25번째 바이트는 Direction, 26~~27번째는 BulletType 등 메세지를 필드로 나눈다

IP 패킷 (OSI 3계층, 특징 알아두면 좋다)

* 주고 받는 데이터(페이로드)의 크기, 송신자 주소, 수신자 주소, checksum 등 포함
* 패킷 하나 크기는 제한돼있음. 이는 라우터마다 다름.
* 스트림이나 메시지에선 이런 제한 없음.
* 단편화(fragmentation) : 긴 스트림 같은거 송신할 때 패킷 크기 제한 맞춰서 여러 조각냄

### 2.4 컴퓨터 네트워크 식별자

IPv4 : 4바이트 IPv6 : 16바이트 포트 : 2바이트 (65535 이하)

host name -- DNS 서버 -> IP 주소

### 2.5 컴퓨터 네트워크의 품질과 특성

#### 네트워크의 품질을 저해하는 것들

스위치나 라우터에 처리 가능 한계 넘는 데이터 수신되면

* 1 패킷 드롭 : 한계 넘는 건 버린다
* 2 처리 못한 건 메모리에 누적

패킷 드롭 하면 패킷 유실(packet loss) 일어남 인터넷 서비스 공급자는 초과분 버리는 편 가정용 라우터는 메모리 누적시키는 편 누적이 지속되면 멈추거나 재부팅

OSI 계층 1에서 데이터가 아날로그 신호로 변환 수신자가 다시 디지털로 바꿀 때 잡음 섞이거나 신호 약해질 수 있음 계층 2, 3의 체크섬 검사로 확인 가능. 수정 시도 후 안되면 버림.

정리

* 네트워크 기기 처리 한계 넘어가면 패킷 유실 발생 가능
* 회선 신호가 약하거나 잡음 섞이면 패킷 유실 발생 가능

#### 전송 속도와 전송 지연 시간

전송 속도(스루풋) : 두 기기 간에 초당 전송될 수 있는 최대 데이터양 레이턴시 : 두 기기 간에 데이터를 최소량 전송할 때 걸리는 시간

레이턴시 영향 주는 것

* 매체 종류 : 구리 전선(랜선), 광섬유(해저 케이블), 무선
* 매체 품질 : 선 훼손, 신호 간섭 등 일어나면 패킷 드롭 일어나 재전송
* 송수신자 사이의 라우터 처리 속도
* 데이터의 매체 통과 시간보다 네트워크 기기 처리 시간이 훨씬 길다

두 단말기 사이의 레이턴시 = 두 단말기 사이에 있는 네트워크 기기의 레이턴시 총합 두 단말기 사이의 스루풋 = 두 단말기 사이의 네트워크 기기 중 최소 스루풋

#### 무선 네트워크의 품질

와이파이 데이터 전송 (CSMA, Carrier Sense Multiple Access)

* 1 데이터를 전파로 변환하여 보내기 직전 다른 전파 감지되는지 확인
* 2 전파 감지 안되면 전파 전송
* 3 전파 감지되면 기다렸다가 1,2 반복
* 4 신호 보낸 후 '신호 받았음' 응답 오는지 체크
* 5 응답 일정 시간 없으면 다시 보냄

이게 없으면 서로의 신호가 뒤섞여 통신 불가

### 2.6 컴퓨터 네트워크에서 데이터 보내기와 받기

UDP와 TCP는 OSI 계층 4

UDP (User Datagram Protocol) 네트워킹

* datagram : 64킬로 바이트 이하의 이진 데이터
* 패킷 유실, 중복, 순서 뒤섞임 발생 가능 (훼손은 매우 드묾)
* UDP 소켓 하나로 데이터 송수신 모두 가능
* 다대다 통신 가능

TCP (Transmission Control Protocol) 네트워킹

* TCP에서 보낼 스트림 데이터는 segment(IP 패킷에 넣을 수 있는 크기의 단위)로 쪼개진 후 IP 패킷에 넣어진다
* ack(확인 응답) 올 때까지 세그먼트 보낸다

### 2.7 패킷 유실 시 UDP와 TCP에서 현상

UDP에서 패킷 하나 유실되면 그 패킷이 포함된 datagram 전부 유실 TCP에서 패킷 하나 유실되면 (ack 안 오면) 그 패킷만 재전송

UDP 평균 레이턴시 = 네트워크 기기 레이턴시 TCP 평균 레이턴시 = 네트워크 기기 레이턴시 + 패킷 유실률 x 재전송 대기 시간

하지만 네트워크 게임에선 TCP가 더 불쾌 패킷 유실률은 20% 이하지만, 재전송 대기 시간은 수백 밀리초 수준

### 2.9 네트워크 주소 변환

네트워크 주소 변환(Network Address Translation, NAT)

* 다른 단말기로 전송되던 패킷의 송신자 주소나 수신자 주소가 다른 걸로 변환
* NAT 라우터 : NAT 변환하는 기기. 인터넷 공유기 같은 것들.
  * IP 주소 1개를 여러 기기가 공용하게 만들어준다
  * 홀펀칭 : 임의의 포트 번호 할당해서 내부 주소에 대응하는 외부 주소를 만들어 포트 매핑 엔트리에 저장

## 3장 소켓 프로그래밍

소켓 비동기 입출력 방식

* 논블로킹 소켓 : 기다리지 않는 소켓
* Overlapped I/O : 한 소켓에서 여러 I/O 요청
* epoll : 소켓 변화 생기면 반환
* IOCP (I/O Completion Port) : 입력 오면 스레드풀이 일함

### 3.3 블로킹과 소켓 버퍼

send()는 소켓 버퍼 꽉 차지 않는 이상 블로킹 없이 바로 반환 송신 버퍼는 디폴트로 수천 바이트를 담을 수 있다

### 3.5 수신 버퍼가 가득 차면 발생하는 현상

수신 버퍼 꽉 차면 TCP 데이터 보내는 쪽에선 send() 블로킹됨 보냈는데 여유 공간 없으면 데이터그램 버려짐 TCP는 수신 가능 데이터양이 송신 속도보다 낮으면 알아서 송신량 줄임

UDP는 꽉 차면 그냥 계속 보내고 계속 버려짐 이 때문에 라우터가 패킷을 버려서 주변 네트워킹 두절되기도 함

### 3.6 논블록 소켓

네트워킹 해야 하는 대상 여럿일 때

네트워킹 대상 개수만큼 스레드 만들기 : 스레드 개수 1000개면 각 스레드 차지하는 호출 스택이 1메가이므로 총 1기가 컨텍스트 스위치도 대량 발생

논블록 소켓 : 논블록 수신으로 소켓들 루프 돌기

TCP일 때 connect()가 논블로킹이라면 0바이트 송신해서 연결 됐는지 안됐는지 확인해야함 서버가 busy waiting 방식으로 사용량 100% 찍으며 기다리면 곤란하니까 select()나 epoll() 사용

### 3.7 Overlapped I/O 혹은 비동기 I/O

논블록 소켓 장점

* 스레드 블로킹 없으므로 중도 취소 같은 통제 가능
* 스레드 개수가 1개여도 소켓 여러 개 다룰 수 있음
* 스레드 개수 적어서 연산량, 호출 스택 낭비 안됨

논블록 소켓 단점

* 변화 없으면 재시도 호출 낭비 발생 : UDP는 송신 버퍼 크기보다 보낼 데이터가 크면 못 보내고 헛발질만 계속함
* 소켓 입출력 함수 호출 시 입력하는 데이터 블록 복사 연산 발생 : 캐시 미스되면 RAM 액세스 해야될 때도 있음
* send(), receive()는 재시도 호출 API가 일관되지 않다

Overlapped I/O

* 윈도 전용
* Overlapped I/O 함수는 즉시 리턴되지만, 운영체제가 별도로 해당 I/O 진행
  * 운영체제는 소켓 함수 인자로 넣었던 데이터 블록과 Overlapped Status 객체를 백그라운드에서 액세스
  * 따라서 완료되기 전까지 두 객체 수정/삭제하면 안됨
* Overlapped I/O 전용 송수신 함수 호출하면 운영체제는 데이터 있는 메모리 블록 자체를 송신 버퍼로 사용

Overlapped I/O 장점

* 재시도 호출 낭비 없다
* 데이터 블록 복사 연산 없앤다

### 3.8 epoll

epoll은 I/O 가능일 때만 루프 돈다. 성능 개선될 것 같지만, 현실에선 대부분 송신 버퍼에 데이터 계속 주어진다.

레벨 트리거 : 소켓이 I/O 가능함 엣지 트리거 : I/O 가능 아니었는데, 가능해졌다

엣지 트리거 쓸 땐 여러 소켓의 데이터 다 꺼내도록 반복


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lazyartisan.gitbook.io/note/main-page/books/undefined/1-3.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
