5114 words
26 minutes
260119_Async_Rust001

link#


좋은 자료 모음#



0. asycn Rust를 이해하기 위한 기초 지식|🔝|#

  • 다음의 개념들에 대한 기본 지식: 기본 Rust 문법, Trait, Associated Type, Generics, Lifetime
  • 기본적인~ 중상의 Rust 이해도
  • 컴퓨터 구조에 대한 간단한 이해(CPU, 메모리, I/O, …)
  • Rust에서 async/await을 사용해 본 사람

1.1 Rust 1.75부터 async trait공식 지원됨.|🔝|#

// No special attributes needed on stable Rust (>= 1.75)

struct MyService {
    data: String,
}

trait Service {
    async fn serve(&self, input: String) -> String;
}

impl Service for MyService {
    async fn serve(&self, input: String) -> String {
        //  input + " processed"
        // ... async logic using .await
        format!("{}: {}", self.data, input)
    }
}

#[tokio::main]
async fn main() {
    // 1. Create an instance of MyService
    let service = MyService {
        data: "MyDataService".to_string(),
    };

    // 2. Call the async serve method via the Service trait
    let result = service.serve("Hello".to_string()).await;
    println!("Result: {}", result);
}

1.2 tokio를 cargo expand 해보면 결국 synchronous 환경에서 코드가 돌아가고 있다.|🔝|#

  • 좀 충격적이다.~
#[tokio::main]
async fn main() {
    println!("Hello, world!");
}
  • cargo expand
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
fn main() {
    let body = async {
        {
            ::std::io::_print(format_args!("Hello, world!\n"));
        };
    };
    #[allow(
        clippy::expect_used,
        clippy::diverging_sub_expression,
        clippy::needless_return,
        clippy::unwrap_in_result
    )]
    {
        return tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("Failed building the Runtime")
            .block_on(body);
    }
}

1.3 보통 asynchronous코드와 synchronous코드를 상황에 따라서 잘 섞어서 쓰면된다.|🔝|#

  • 결국 trade off 관계라.. 절대적으로 뭘 써야한다는건 틀린 이야기.
    • 최적화를 하면서 제일 빠른걸로 골라쓰면된다. ^^

1.4 (deep dive async)async desugar를 경험해서 더 깊게 파 보자|🔝|#

async fn compute() -> i32 {
    5
}

fn desugar_compute() -> impl Future<Output = i32> {
    async move { 5 }
}

#[tokio::main]
async fn main() {
    let res1 = compute().await;

    let desugar_res = desugar_compute().await;

    println!("{res1}");
    println!("{desugar_res}");
}

2.1 비동기 프로그래밍이 필요한 시점|🔝|#

  • C10K Problem : 10,000개의 클라이언트와 동시에 소통할 수 있는가?
  • 쉽지 않다.
    • 만악의 근원 : Synchronous blocking I/O
    • I/O를 하는 동안에는 프로세스가 ‘동작그만’
      • CPU 자원이 놀게 됨.
      • 아무리 서버 성능을 올려도 수용량 한계
    • I/O-bound

2.2 I/O models|🔝|#

  • Figure 1. Simplified matrix of basic Linux I/O models
Image
  • https://developer.ibm.com/articles/l-async/

  • Let’s explore the different I/O models that are available under Linux. This isn’t intended as an exhaustive review, but rather aims to cover the most common models to illustrate their differences from asynchronous I/O. Figure 1 shows synchronous and asynchronous models, as well as blocking and non-blocking models.

  • Linux에서 사용할 수 있는 다양한 I/O 모델을 살펴봅시다. 이는 철저한 검토를 위한 것이 아니라 비동기 I/O와의 차이점을 설명하기 위해 가장 일반적인 모델을 다루는 것을 목표로 합니다. 그림 1은 동기화 및 비동기화 모델뿐만 아니라 차단 및 비차단 모델도 보여줍니다.

2.3 Synchronous blocking I/O|🔝|#

  • Figure 2. Typical flow of the synchronous blocking I/O model
    • 그림 2. 동기식 차단 I/O 모델의 일반적인 흐름
Image
  • Figure 2 illustrates the traditional blocking I/O model, which is also the most common model used in applications today. Its behaviors are well understood, and its usage is efficient for typical applications. When the read system call is invoked, the application blocks and the context switches to the kernel. The read is then initiated, and when the response returns (from the device from which you’re reading), the data is moved to the user-space buffer. Then the application is unblocked (and the read call returns).

  • One of the most common models is the synchronous blocking I/O model. In this model, the user-space application performs a system call that results in the application blocking. This means that the application blocks until the system call is complete (data transferred or error). The calling application is in a state where it consumes no CPU and simply awaits the response, so it is efficient from a processing perspective.

  • 그림 2는 오늘날 애플리케이션에서 가장 일반적으로 사용되는 모델인 전통적인 차단 I/O 모델을 보여줍니다. 이 모델의 동작은 잘 이해되어 있으며 일반적인 애플리케이션에서 효율적으로 사용됩니다. 읽기 시스템 호출이 호출되면 애플리케이션이 차단되고 컨텍스트가 커널로 전환됩니다. 그런 다음 읽기가 시작되고 응답이 반환되면(읽기 중인 디바이스에서) 데이터가 사용자 공간 버퍼로 이동합니다. 그런 다음 애플리케이션의 차단이 해제되고 읽기 호출이 반환됩니다.

  • 가장 일반적인 모델 중 하나는 동기식 차단 I/O 모델입니다. 이 모델에서 사용자 공간 애플리케이션은 시스템 호출을 수행하여 애플리케이션을 차단합니다. 즉, 애플리케이션은 시스템 호출이 완료될 때까지 차단합니다(데이터 전송 또는 오류). 호출 애플리케이션은 CPU를 소모하지 않고 단순히 응답을 기다리는 상태이므로 처리 관점에서 효율적입니다.

2.4 Typical flow of the synchronous non-blocking I/O model|🔝|#

  • Figure 3. Typical flow of the synchronous non-blocking I/O model
    • 그림 3. 동기식 비차단 I/O 모델의 일반적인 흐름
Image
  • The implication of non-blocking is that an I/O command may not be satisfied immediately, requiring that the application make numerous calls to await completion. This can be extremely inefficient because in many cases the application must busy-wait until the data is available or attempt to do other work while the command is performed in the kernel. As also shown in Figure 3, this method can introduce latency in the I/O because any gap between the data becoming available in the kernel and the user calling read to return it can reduce the overall data throughput.
    • 비차단의 의미는 I/O 명령이 즉시 충족되지 않을 수 있으므로 애플리케이션이 완료를 대기하기 위해 수많은 호출을 해야 한다는 것입니다. 이는 많은 경우 애플리케이션이 데이터가 사용 가능할 때까지 대기하거나 명령이 커널에서 수행되는 동안 다른 작업을 시도해야 하기 때문에 매우 비효율적일 수 있습니다. 그림 3에서도 볼 수 있듯이, 이 방법은 커널에서 사용 가능해지는 데이터와 사용자가 읽기를 호출하여 반환하는 것 사이의 간격이 생기면 전체 데이터 처리량을 줄일 수 있기 때문에 I/O에 지연 시간을 초래할 수 있습니다.

2.5 Typical flow of the asynchronous blocking I/O model (select)|🔝|#

  • Figure 4. Typical flow of the asynchronous blocking I/O model (select)
    • 그림 4. 비동기 차단 I/O 모델의 일반적인 흐름(선택)
Image
  • The primary issue with the select call is that it’s not very efficient. While it’s a convenient model for asynchronous notification, its use for high-performance I/O is not advised.
    • 선택 통화의 주요 문제는 효율성이 낮다는 것입니다. 비동기식 알림을 위한 편리한 모델이지만 고성능 입출력에는 사용하지 않는 것이 좋습니다.

2.6 Async와 Nonblocking은 (미묘하게) 다릅니다.|🔝|#

  • Async: I/O를 하는 동안 다른 일을 할 수 있다
  • Nonblocking: I/O가 준비될때까지 기다리지 않는다
  • 하지만 사용하는 맥락마다 뜻이 다르고 섞어서 쓰는 곳도 많으니 대충 넘어갑니다.
    • 위 글은 원본 내용이고 내가 이해한거
    • Async(Concurrency) VS Sync(우리가 생각하는 병렬)

2.7 Concurrency VS Parallelization|🔝|#

  • 동시성 vs 병렬화

  • Parallelization(병렬화)

    • “simultaneous execution”
    • CPU 코어 개수만큼 병렬로 진행함.
    • 최소 스레드 2개 이상 필요함
  • Concurrency(동시성)

    • 여러 작업을 ‘같은 시간 동안’ 진행(시분할, 병렬화, 코루틴…), “simultaneous wating”
    • 스레드 1개로도 가능

2.8 Asynchronous Rust써보기|🔝|#

cargo add tokio -F full
async fn async_print_txt(data: Result<Vec<u8>, std::io::Error>) {
    match data {
        Ok(contents) => println!("File contents: {:?}", String::from_utf8(contents)),
        Err(e) => eprintln!("Error reading file: {}", e),
    }
}

#[tokio::main]
async fn main() {
    let data = tokio::fs::read("wow.txt").await;
    let handle = tokio::spawn(async move {
        async_print_txt(data).await;
    });
    handle.await.unwrap();
}
  • asynchronous Rust 간단한 규칙들
    • async fnasync fn안에서만 호출 가능하다.

    • async fn foo()을 호출할 때는 await을 해줍니다.

      • foo() 의 결과를 받고 싶으면 foo().await;
      • 안 하면 경고해 주니깐 까먹어도 됩니다.
    • async fn 안에서는 std의 I/O 함수를 호출하지 않는다.

      • 대신 tokio의 것을 쓴다.(tokio::fs, tokio::net..)
      • 만약에 std의 것을 써야 한다면 tokio::task::spawn_bloking으로 감쌉니다.
      • async fn 이 호출하는 모든 함수에 대해서도 동일한 규칙이 적용.
    • 그냥 fn에서 async fn 호출 하는것이 가능하긴 함.

      • Runtime::new().block_on()
      • Runtime::spawn()

2.9 Synchronous Rust써보기|🔝|#

use std::thread;

fn main() {
    let data = std::fs::read("wow.txt");
    let handle = thread::spawn(move || {
        match data {
            Ok(contents) => println!("File contents: {:?}", String::from_utf8(contents)),
            Err(e) => eprintln!("Error reading file: {}", e),
        }
    });

    handle.join().unwrap();
}

2.10 다른 언어와 비교|🔝|#

  • Go : Goroutines

    • Go런타임이 관리하는 초경량 스레드
    • Mthreading
  • Javascript : Promises

    • N:1 threading
  • Rust : Futures

    • Executor 구현 마음대로(3rd-party)
    • tokio::rt(N:1), rt-multi-thread(M)
    • 중요한 점 : Future.await되지 않는 한 스스로 실행되지 않음.
      • 참고로 tokio::task::spawn은 스스로 실행됩니다.
  • Future cooperative하게 처리됨.( ↔ preemptive)

3.1 결국 Future trait를 이해해야한다. 더 깊이 들어가보자.|🔝|#

trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

3.2 Future 정의를 좀더 단순화 해보자.|🔝|#

trait Future {
    type Output;
    fn poll(&mut self) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}

3.3 The Future Trait|🔝|#

  • type Output : Future(를 구현하는 어떤 친구)가 반환할 값의 타입
  • fn poll() : Future(를 구현하는 어떤 친구) 야 ‘준비되었니?’
    • Poll::Ready(Output) : 응 ‘준비됨.’(Output을 돌려주며)
    • Poll::Pending : 아니
      • 주의 : 이미 Ready를 반환했으면 다시 poll() 했을 때 어떻게 될지 모름.
      • Undefined Behavior 빼고 모두 가능(no-op, panic!, Pending, Ready)
  • poll() 은 준비 여부에 관계없이 ‘즉시’ 반환합니다.
    • Synchronous Rust는 준비될때까지 계속 대기.
    • 그러면 호출자는 준비가 될 때까지 (더 정확히 : 다시 poll할 때까지) 다른 일을 할 수 있습니다.

3.4 여기서 등장하는게 Waker|🔝|#

  • 효율적으로 poll() 하려면 : 필요할 때만 알려주면 됩니다.
    • 타이머 : 목표한 시간이 됨, 네트워크 : 해당 소켓에 이벤트 발생.
  • 어떻게 알려주나요?
let waker = cx.waker().clone();
waker.wake();
  • cx: &mut Context<'_> 바로 여기.!!
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

3.5 Future는 누가 실행해주나요?|🔝|#

  • 이걸 Executor라 함.

  • Poll::Ready 를 받으면 결과가 나왔으니 반환해주고

  • Poll::Pending 을 받으면 wake 될때까지 기다리다가 다시 poll() 하면 됩니다.

  • 예시 코드

use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

static VTABLE: RawWakerVTable = RawWakerVTable::new(
    |clone_me| unsafe {
        let arc = Arc::from_raw(clone_me as *const Park);
        std::mem::forget(arc.clone());
        RawWaker::new(Arc::into_raw(arc) as *const (), &VTABLE)
    },
    |wake_me| unsafe { unpark(&Arc::from_raw(wake_me as *const Park)) },
    |wake_by_ref_me| unsafe { unpark(&*(wake_by_ref_me as *const Park)) },
    |drop_me| unsafe { drop(Arc::from_raw(drop_me as *const Park)) },
);

#[derive(Default)]
struct Park(Mutex<bool>, Condvar);

fn unpark(park: &Park) {
    *park.0.lock().unwrap() = true;
    park.1.notify_one();
}

/// Run a `Future`.
pub fn run<F: std::future::Future>(mut f: F) -> F::Output {
    let mut f = unsafe { std::pin::Pin::new_unchecked(&mut f) };
    let park = Arc::new(Park::default());
    let sender = Arc::into_raw(park.clone());
    let raw_waker = RawWaker::new(sender as *const _, &VTABLE);
    let waker = unsafe { Waker::from_raw(raw_waker) };
    let mut cx = Context::from_waker(&waker);

    loop {
        match f.as_mut().poll(&mut cx) {
            Poll::Pending => {
                let mut runnable = park.0.lock().unwrap();
                while !*runnable {
                    runnable = park.1.wait(runnable).unwrap();
                }
                *runnable = false;
            }
            Poll::Ready(val) => return val,
        }
    }
}

3.6 Wrap-up : async은 ‘문법적 설탕’|🔝|#

  • trait Future은 :

    • ‘즉시’ 값을 반환하지 않는 값을 추상화하는 트레이트
    • 값을 반환할 수 있는지 poll() 로 확인
    • 반환되는 값의 타입은 associated type Future::Output
  • async fn(Args) -> Return 은:

    • fn(Args) -> impl Future<Output = Return> 의 설탕
  • async { ... } 은:

    • impl Future<Output=T> 의 설탕
  • recap : impl Trait 이란?

  • 어떤 값이 Trait 을 구현한다는 사실만 남겨둔 ‘가면 쓴 (Opaque) 타입’

  • 컴파일 타임에 타입이 추론되지만 정확히 어떤 타입인지 이름을 알수 없음.

  • 이게 없으면: xxx.rs의 6번째 줄부터 16번째 줄까지의 async {} 에 타입을 뭘로 줘야 할까요?

4.1 여기까지 쉬운데 골치 아픈게 등장 바로 Pin|🔝|#

  • Pin
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
  • 공식 문서를 보자

  • Types that pin data to a location in memory.

  • It is sometimes useful to be able to rely upon a certain value not being able to move, in the sense that its address in memory cannot change. This is useful especially when there are one or more pointers pointing at that value. The ability to rely on this guarantee that the value a pointer is pointing at (its pointee) will

      1. Not be moved out of its memory location
      1. More generally, remain valid at that same memory location
  • 데이터를 메모리의 위치에 고정하는 유형.

  • 특정 값의 메모리 주소가 변경될 수 없기 때문에 특정 값을 움직일 수 없다는 점에 의존할 수 있는 것이 때때로 유용합니다. 이는 특히 해당 값을 가리키는 포인터가 하나 이상 있을 때 유용합니다. 이에 의존할 수 있는 능력은 포인터가 가리키는 값(포인트티)이 다음과 같음을 보장합니다

      1. 메모리 위치에서 이동하지 마십시오
      1. 더 일반적으로 동일한 메모리 위치에서 유효하게 유지합니다

4.2 일단 Future가 본질적으로 무엇인지 다시 알아보자.|🔝|#

  • How Futures are Constructed.

    • Future는 결론적으로 FSM(Finite State Machine) 입니다.
    • poll() : State 간의 전환이 정의된 로직
    1. Core idea (one line)
  • A Future is a state machine that advances only when poll() is called.

    • Now let’s see it.
    1. Simple Future FSM (no .await)
  • Example:

async fn add_one(x: i32) -> i32 {
    x + 1
}
  • FSM diagram
┌────────────┐
│   START    │
│ (not polled)│
└─────┬──────┘
      │ poll()

┌────────────┐
│  COMPLETE  │
│ Ready(val) │
└────────────┘
  • One poll

  • No suspension

  • Immediate transition to Ready

    1. Future with .await (real FSM)
  • Example:

async fn foo() {
    step1().await;
    step2().await;
}
  • FSM diagram (CLI-friendly)
                 poll()
        ┌────────────────────────┐
        ▼                        │
┌────────────┐   Pending   ┌────────────┐
│   START    │────────────▶│ WAIT_STEP1 │
│            │             │ .await #1  │
└────────────┘             └─────┬──────┘
                                 │ poll()

                         ┌────────────┐   Pending
                         │ WAIT_STEP2 │──────────┐
                         │ .await #2  │          │
                         └─────┬──────┘          │
                               │ poll()          │
                               ▼                 │
                         ┌────────────┐◀─────────┘
                         │  COMPLETE  │
                         │ Ready(())  │
                         └────────────┘
  • Every .await introduces:

    • a state
    • a possible suspension point
    1. What poll() really does (FSM view)
    • Each poll() call:
poll():
  if current_state can advance:
      move to next state
  else:
      return Pending
  • CLI pseudo-FSM logic
STATE: START
  poll -> transition to WAIT_STEP1

STATE: WAIT_STEP1
  poll -> Pending (until woken)
  poll -> transition to WAIT_STEP2

STATE: WAIT_STEP2
  poll -> Pending (until woken)
  poll -> transition to COMPLETE

STATE: COMPLETE
  poll -> Ready
  • ⚠️ After COMPLETE, polling again is a logic error.

    1. How Pin fits into the picture
    • Here’s the important constraint the diagram hides:
┌──────────────────────────────┐
│  FSM memory address MUST NOT │
│  change between poll() calls │
└──────────────────────────────┘
  • That’s why:
fn poll(self: Pin<&mut Self>, ...)
  • Without Pin, the FSM could move → internal references break.

    1. JoinHandle FSM (bonus)
    • A JoinHandle is also a Future, but a different FSM:
┌────────────┐
│  WAITING   │◀───────────────┐
│ task runs  │                │
└─────┬──────┘                │
      │ task finishes         │
      ▼                       │
┌────────────┐  poll()        │
│  COMPLETE  │───────────────┘
│ Ready(T)   │
└────────────┘
  • It does not do work — it only observes state.

4.3) 7. One-screen “ultimate” summary diagram|🔝|#

           ┌──────────────┐
           │   poll()     │
           ▼              │
┌────────────┐   Pending  │
│   STATE 0  │────────────┤
└─────┬──────┘            │
      │                   │
      ▼                   │
┌────────────┐   Pending  │
│   STATE 1  │────────────┤
└─────┬──────┘            │
      │                   │
      ▼                   │
┌────────────┐            │
│  COMPLETE  │◀───────────┘
│ Ready(val) │
└────────────┘
  • That is a finite state machine.

    1. Final sentence (the “click”)
    • An async fn compiles into a pinned, poll-driven finite state machine whose transitions are controlled by .await.
  • 그것은 유한 상태 기계입니다.

    1. 마지막 문장 (“클릭”)
    • 비동기 fn은 .ait에 의해 전이가 제어되는 고정된 폴 구동 유한 상태 기계로 컴파일됩니다.

4.4 How Futures are Constructed(A Simplified Example)|🔝|#

  • enum/struct : ‘멈춤’과 ‘멈춤’ 사이를 구분할 상태들을 정의
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_'>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}

enum CallerState {
    ListeningForTarget,
    CallingTarget,
    SendingMessageToTarget,
}

impl Future for CallerState {
    fn poll(&mut Self) -> Poll {
        match *self {
            ListeningForTarget => {
                *self = CallingTarget;
                return Peding;
            }
            CallingTarget => {
                *self = SendingMessageToTarget;
                return Peding;
            }
            SendingMessageToTarget => {
                return Ready(())
            }
        }
    }
}
  • 좀 더 현실적으로 : 직원들이 전화기를 사용해 상태를 전달해 줘야함.
    • 이걸 어떻게 표현하지?
    • 생각해 보니 자기참조가 발생.. 이래서 Pin 등장!!!
enum CallerState<'a> {
    ListeningForTarget(&'a mut Telephone),
    CallingTarget(&'a mut Telephone),
    SendingMessageToTarget,
}

struct CallCenterState {
    callers : Vec<CallerState<'what>>,
    telephones: Vec<Telephone>,
}

impl Future for CallCenterState {
    // CallerState를 하나씩 .poll() 해 주기
}

4.5) 지역변수 참조 = 자기 참조|🔝|#

  • 4.5.1 async { ... } 설탕을 써도 자기 참조를 피할 수 없음.
async {
    let telephone = Telephone;
    let caller = Caller(&mut telephone);
    caller.await;
}

struct State {
    var_telephone : Telephone,
    ref_caller : &'what Telephone,
}
  • 4.5.2. ❌ Now make it self-referential (ERROR case)
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct Telephone {
    number: i32,
}

struct CallerFuture<'a> {
    tel: Telephone,
    tel_ref: Option<&'a Telephone>,
    // ❌ self reference
    state: u8,
}

impl<'a> CallerFuture<'a> {
    fn new() -> Self {
        CallerFuture {
            tel: Telephone { number: 777 },
            tel_ref: None,
            state: 0,
        }
    }
}

impl<'a> Future for CallerFuture<'a> {
    type Output = ();
    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.state == 0 {
            let tel_ptr: *const Telephone = &self.tel;
            // ❌ cannot create reference tied to self like this
            self.tel_ref = Some(unsafe { &*tel_ptr });
            self.state = 1;
            return Poll::Pending;
        }
        println!("number = {}", self.tel_ref.unwrap().number);
        Poll::Ready(())
    }
}

#[tokio::main]
async fn main() {
    let fut = CallerFuture::new();
    // move occurs
    let fut2 = fut;
}
  • This is exactly the self-reference problem.

  • 4.5.3 Pin 해결해 보자

    • 4. ✅ Correct solution using Pin (self-referential safe model)
  • Core idea:

Memory must never move after references are created.

  • 4.5.4 해결 code ✅ Proper pinned self-referential struct
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

struct Telephone {
    number: i32,
}

struct CallerFuture {
    tel: Telephone,
    tel_ptr: *const Telephone, // raw self-reference
    state: u8,
}

impl CallerFuture {
    fn new() -> Pin<Box<Self>> {
        let mut fut = Box::pin(CallerFuture {
            tel: Telephone { number: 777 },
            tel_ptr: std::ptr::null(),
            state: 0,
        });

        // initialize self-reference AFTER pinning
        let tel_ptr = &fut.tel as *const Telephone;

        unsafe {
            let fut_mut = Pin::get_unchecked_mut(fut.as_mut());
            fut_mut.tel_ptr = tel_ptr;
        }

        fut
    }
}

impl Future for CallerFuture {
    type Output = ();

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
        let this = unsafe { self.get_unchecked_mut() };

        if this.state == 0 {
            this.state = 1;
            return Poll::Pending;
        }

        unsafe {
            println!("tel address inside future = {:p}", this.tel_ptr);
            println!("number = {}", (*this.tel_ptr).number);
        }

        Poll::Ready(())
    }
}

// minimal dummy waker
fn dummy_waker() -> Waker {
    unsafe fn clone(_: *const ()) -> RawWaker {
        RawWaker::new(std::ptr::null(), &VTABLE)
    }
    unsafe fn wake(_: *const ()) {}
    unsafe fn wake_by_ref(_: *const ()) {}
    unsafe fn drop(_: *const ()) {}

    static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);

    unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}

fn main() {
    let mut fut = CallerFuture::new();

    println!("future address (pinned) = {:p}", &*fut);
    println!("tel address (direct)    = {:p}", &fut.tel);

    let waker = dummy_waker();
    let mut cx = Context::from_waker(&waker);

    // first poll
    assert!(matches!(fut.as_mut().poll(&mut cx), Poll::Pending));

    // second poll
    assert!(matches!(fut.as_mut().poll(&mut cx), Poll::Ready(())));
}
  • result ✅ Pin으로 성공한 코드(That’s what Pin guarantees.)
future address (pinned) =   0x7fffd7ac47f0
tel address (direct)    =   0x7fffd7ac47f8
tel address inside future = 0x7fffd7ac47f8
number = 777

4.6. Why Pin exists (shown visually)|🔝|#

❌ BAD (moving future breaks pointers)

[ addr 0x1000 ] FUTURE ──┐
                         ├─ move → 0x9000 💥
[ addr 0x1000 ] a, b ----┘
✅ GOOD (Pin)

[ addr 0x1000 ] FUTURE (IMMOBILE)
  ├─ state
  ├─ a
  ├─ b
  └─ child futures
async fn example() {
    let a = 1;
    step1().await;
    let b = 2;
    step2().await;
}
    1. High-level picture
┌────────────────────────────────────┐
│            FUTURE (HEAP)            │
│                                    │
│  ┌──────────────┐                  │
│  │   STATE      │                  │
│  │--------------│                  │
│  │ START        │                  │
│  │ WAIT_STEP1   │◀───┐             │
│  │ WAIT_STEP2   │    │ poll()       │
│  │ COMPLETE     │────┘             │
│  └──────────────┘                  │
│                                    │
│  ┌──────────────┐                  │
│  │   MEMORY     │  ← survives      │
│  │--------------│     across polls │
│  │ a: i32 = 1   │                  │
│  │ b: i32 = ?   │                  │
│  │ step1_fut    │                  │
│  │ step2_fut    │                  │
│  └──────────────┘                  │
│                                    │
│  (Pinned: address must not change) │
└────────────────────────────────────┘
    1. One-screen “ultimate” diagram

           poll()
     ┌────────────────┐
     ▼                │
┌─────────┐  Pending  │
│ STATE 0 │───────────┤
│ a = 1   │           │
└────┬────┘           │
     ▼                │
┌─────────┐  Pending  │
│ STATE 1 │───────────┤
│ step1   │           │
└────┬────┘           │
     ▼                │
┌─────────┐  Pending  │
│ STATE 2 │───────────┤
│ b = 2   │           │
└────┬────┘           │
     ▼                │
┌─────────┐◀──────────┘
│ DONE    │
│ Ready() │
└─────────┘

(All state + memory live inside ONE pinned Future)

4.7. Wrap-up : Pin이 필요한 이유|🔝|#

  • Future는 ‘중간중간 멈추는’ 유한 상태 기계인데

    • Future 의 상태에 자신을 가리키는 참조가 섞여 있을 수 있고
    • 자기 참조가 있으면 ‘움직이는’ 순간 참조가 무료화되니깐
    • 자기 참조를 가진 Future는 절대 움직일 수 없게 해야 하는데
    • 이걸 Rust 문법 상으로 보장할 수 없다.!(대입/호출/반환 등등이 죄다 move)
  • Q: 자기 참조가 없으면 움직여도 되나요?

    • A : 네
    • 그래서 Unpin trait이 있습니다. (뒤에 나옴.)

다음 주제|🔝|#


관련 블로그 글#

260119_Async_Rust001
https://younghakim7.github.io/blog/posts/260119_async_rust001/
Author
YoungHa
Published at
2026-01-19