Tokio: Rust async programming
Tokio is a library or runtime for asyncronous programming in Rust.
Applicable: efficiently make network service, web server, async process.
What is async, Why you need Tokio
Rust의 기본 실행 방식은 동기(synchronous) 실행이야.
즉, 한 작업이 끝나야 다음 작업이 실행되지.
하지만 비동기(async) 방식은 작업을 중단하지 않고 동시에 실행할 수 있어.
예를 들어, 다음과 같은 상황에서 비동기가 필요해:
✅ 네트워크 요청 (예: HTTP API 호출)
✅ 데이터베이스 쿼리
✅ 파일 읽기/쓰기
✅ 타이머/지연 작업
Tokio는 이런 비동기 작업을 효과적으로 실행하는 런타임을 제공해.
2️⃣ 기본적인 Tokio 실행 방식
Rust에서 비동기 코드를 실행하려면 비동기 런타임이 필요해.
Tokio는 #[tokio::main]을 사용해 비동기 실행을 도와줘.
🛠 Tokio 기본 코드
rust
복사
편집
use tokio::time::{sleep, Duration};
#[tokio::main] // 비동기 실행을 위한 매크로
async fn main() {
println!("작업 시작!");
sleep(Duration::from_secs(2)).await; // 2초 기다림 (비동기)
println!("2초 후 작업 완료!");
}
🔹 #[tokio::main] → Tokio 런타임을 자동으로 생성
🔹 async fn main() → 비동기 함수로 실행
🔹 .await → 해당 작업이 끝날 때까지 기다림
위 코드를 실행하면 2초 동안 기다렸다가 다음 작업을 실행하는 걸 볼 수 있어.
3️⃣ Tokio의 실행 방식
Tokio는 크게 싱글스레드와 멀티스레드 런타임을 제공해.
여기서 중요한 개념이 태스크(Task)야.
🔹 태스크(Task)란?
태스크는 쉽게 말해 "비동기 함수 단위"야.
Rust에서 비동기 함수는 실행만 해서는 동작하지 않아.
tokio::spawn()을 사용해 실행해야 해.
4️⃣ tokio::spawn()으로 태스크 실행하기
Tokio에서는 여러 비동기 작업을 동시에 실행할 수 있어.
tokio::spawn()을 사용하면 멀티태스킹이 가능해!
🛠 태스크 생성 코드
rust
복사
편집
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
println!("비동기 태스크 시작!");
sleep(Duration::from_secs(3)).await;
println!("비동기 태스크 종료!");
});
println!("메인 함수 실행 중...");
handle.await.unwrap(); // 태스크 완료까지 대기
}
📌 실행 순서:
1️⃣ tokio::spawn()으로 새로운 태스크 생성 (3초 대기)
2️⃣ 메인 함수는 멈추지 않고 "메인 함수 실행 중..." 출력
3️⃣ 3초 후, 비동기 태스크가 완료되며 "비동기 태스크 종료!" 출력
📌 handle.await.unwrap();
tokio::spawn()의 결과는 JoinHandle을 반환해.
.await을 붙이면 해당 태스크가 끝날 때까지 기다려줘.
5️⃣ 여러 태스크를 동시에 실행하기
Tokio는 여러 개의 작업을 병렬로 실행할 수 있어.
이를 "Future 병렬 실행"이라고 해.
🛠 여러 태스크 동시에 실행하기
rust
복사
편집
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async {
sleep(Duration::from_secs(3)).await;
println!("Task 1 완료!");
});
let task2 = tokio::spawn(async {
sleep(Duration::from_secs(2)).await;
println!("Task 2 완료!");
});
task1.await.unwrap();
task2.await.unwrap();
println!("모든 작업 완료!");
}
📌 실행 순서:
1️⃣ Task 1과 Task 2를 동시에 실행
2️⃣ Task 2는 2초 후 완료, Task 1은 3초 후 완료
3️⃣ 모든 작업이 끝난 후 "모든 작업 완료!" 출력
6️⃣ join!을 사용한 동시에 실행
위의 코드에서 tokio::spawn()을 쓰면 태스크가 별도 스레드에서 실행돼.
하지만 join!을 사용하면 한 스레드에서 여러 작업을 병렬 실행할 수 있어.
🛠 join!을 활용한 코드
rust
복사
편집
use tokio::{time::{sleep, Duration}, join};
async fn task1() {
sleep(Duration::from_secs(3)).await;
println!("Task 1 완료!");
}
async fn task2() {
sleep(Duration::from_secs(2)).await;
println!("Task 2 완료!");
}
#[tokio::main]
async fn main() {
join!(task1(), task2());
println!("모든 작업 완료!");
}
📌 실행 순서:
1️⃣ task1()과 task2()를 동시에 실행
2️⃣ Task 2가 2초 후 완료
3️⃣ Task 1이 3초 후 완료
4️⃣ 모든 작업이 끝난 후 "모든 작업 완료!" 출력
✅ tokio::spawn()과 join!의 차이
tokio::spawn() → 멀티스레드 실행 (별도 태스크로 실행됨)
join! → 한 스레드에서 동시에 실행 (비동기 병렬 실행)
7️⃣ select!으로 가장 먼저 끝난 작업 실행
Tokio에서는 가장 먼저 끝난 작업을 실행하고 나머지는 취소하는 기능이 있어.
이걸 tokio::select!라고 해.
🛠 select! 예제
rust
복사
편집
use tokio::{time::{sleep, Duration}, select};
#[tokio::main]
async fn main() {
let task1 = async {
sleep(Duration::from_secs(3)).await;
println!("Task 1 완료!");
};
let task2 = async {
sleep(Duration::from_secs(2)).await;
println!("Task 2 완료!");
};
select! {
_ = task1 => println!("Task 1이 먼저 끝남"),
_ = task2 => println!("Task 2가 먼저 끝남"),
}
}
📌 실행 결과:
task2가 2초 후 완료 → "Task 2 완료!" 출력
"Task 2가 먼저 끝남" 출력
task1은 실행이 취소됨
✅ select!은 가장 빨리 끝난 작업만 실행하고 나머지는 무시함.
🔹 정리
✅ #[tokio::main] → 비동기 실행을 위한 매크로
✅ tokio::spawn() → 태스크를 멀티스레드에서 실행
✅ join! → 여러 작업을 동시에 실행
✅ select! → 가장 먼저 끝난 작업 실행
🔹 Actix-web: 고성능 Rust 웹 프레임워크
1️⃣ Actix-web이란?
Actix-web은 Rust에서 고성능 웹 서버를 구축할 수 있도록 도와주는 웹 프레임워크야.
Node.js의 Express, Python의 FastAPI 같은 역할을 해.
Tokio 기반의 비동기 처리를 지원하기 때문에 매우 빠르고 효율적이야.
2️⃣ Actix-web 기본 프로젝트 설정
Actix-web을 사용하려면 먼저 프로젝트를 생성하고 필요한 의존성을 추가해야 해.
📌 프로젝트 생성 및 설치
sh
복사
편집
cargo new actix_example
cd actix_example
📌 Cargo.toml에 Actix-web 추가
toml
복사
편집
[dependencies]
actix-web = "4"
그리고 다음 명령어로 빌드해:
sh
복사
편집
cargo build
3️⃣ Actix-web 기본 서버 만들기
🛠 기본 웹 서버 코드
rust
복사
편집
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello, Actix!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().route("/", web::get().to(hello)))
.bind("127.0.0.1:8080")?
.run()
.await
}
📌 실행 방법:
sh
복사
편집
cargo run
웹 브라우저에서 http://127.0.0.1:8080/ 접속하면 "Hello, Actix!"가 출력될 거야.
4️⃣ 여러 개의 엔드포인트 추가하기
웹 서버는 여러 개의 API를 가질 수 있어.
예제에서는 GET /hello와 GET /bye 엔드포인트를 만들어볼게.
🛠 여러 API 라우트 설정
rust
복사
편집
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello, Actix!")
}
async fn bye() -> impl Responder {
HttpResponse::Ok().body("Goodbye, Actix!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/hello", web::get().to(hello))
.route("/bye", web::get().to(bye))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
📌 실행 후:
http://127.0.0.1:8080/hello → "Hello, Actix!"
http://127.0.0.1:8080/bye → "Goodbye, Actix!"
5️⃣ JSON 응답 처리
일반적으로 REST API는 JSON 형식의 응답을 반환해.
Actix-web에서는 serde를 사용해서 JSON을 쉽게 처리할 수 있어.
📌 Cargo.toml에 serde 추가:
toml
복사
편집
serde = { version = "1", features = ["derive"] }
serde_json = "1"
🛠 JSON 응답 API 만들기
rust
복사
편집
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use serde::Serialize;
#[derive(Serialize)]
struct Message {
message: String,
}
async fn hello_json() -> impl Responder {
let msg = Message {
message: "Hello, Actix with JSON!".to_string(),
};
HttpResponse::Ok().json(msg)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().route("/json", web::get().to(hello_json))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
📌 실행 후:
sh
복사
편집
curl http://127.0.0.1:8080/json
📌 결과:
json
복사
편집
{"message":"Hello, Actix with JSON!"}
6️⃣ POST 요청 처리
웹 서버는 POST 요청을 처리할 수도 있어.
사용자로부터 데이터를 입력받고 처리하는 API를 만들어볼게.
📌 Cargo.toml에 serde 추가:
toml
복사
편집
serde = { version = "1", features = ["derive"] }
🛠 POST 요청 핸들링
rust
복사
편집
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
name: String,
}
async fn greet(user: web::Json) -> impl Responder {
let response = format!("Hello, {}!", user.name);
HttpResponse::Ok().body(response)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/greet", web::post().to(greet))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
📌 실행 후:
sh
복사
편집
curl -X POST -H "Content-Type: application/json" -d '{"name": "Rust"}' http://127.0.0.1:8080/greet
📌 결과:
복사
편집
Hello, Rust!
7️⃣ 웹 서버 상태 관리 (AppState)
웹 서버가 데이터를 유지하려면 상태(State) 관리가 필요해.
예를 들어, API 요청 횟수를 저장하는 카운터를 만들어볼 수 있어.
🛠 AppState를 활용한 상태 관리
rust
복사
편집
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use std::sync::Mutex;
struct AppState {
counter: Mutex,
}
async fn count(state: web::Data) -> impl Responder {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
HttpResponse::Ok().body(format!("Counter: {}", counter))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
counter: Mutex::new(0),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone()) // 상태 공유
.route("/count", web::get().to(count))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
📌 실행 후:
sh
복사
편집
curl http://127.0.0.1:8080/count
📌 결과:
makefile
복사
편집
Counter: 1
한 번 더 요청하면 "Counter: 2"가 출력됨.
🔹 Actix-web 정리
✅ HttpServer::new() → 웹 서버 생성
✅ .route("/path", web::get().to(handler)) → 엔드포인트 추가
✅ HttpResponse::Ok().json() → JSON 응답
✅ web::Json → JSON 요청 처리
✅ web::Data → 상태 관리
🔹 Polars: Rust 데이터 프레임 라이브러리
1️⃣ Polars란?
Polars는 Rust에서 고성능 데이터 프레임 연산을 제공하는 라이브러리야.
Pandas보다 빠르고 병렬 처리가 가능해서 대량의 데이터를 다룰 때 유용해.
✅ Rust에서 빠른 데이터 분석 가능
✅ 멀티스레드 & SIMD 최적화 지원
✅ CSV, JSON, Parquet 파일 처리 가능
✅ Lazy Execution (지연 실행) 지원
2️⃣ 프로젝트 설정 (Polars 설치)
새로운 Rust 프로젝트를 만들고 Polars를 설치해 보자.
📌 프로젝트 생성
sh
복사
편집
cargo new polars_example
cd polars_example
📌 Cargo.toml에 polars 추가:
toml
복사
편집
[dependencies]
polars = { version = "0.37", features = ["lazy", "csv"] }
그리고 다음 명령어로 빌드해:
sh
복사
편집
cargo build
3️⃣ 데이터 프레임 생성하기
Polars의 DataFrame을 사용해서 데이터를 다뤄보자.
🛠 기본적인 데이터 프레임 생성
rust
복사
편집
use polars::prelude::*;
fn main() {
let df = df![
"name" => ["Alice", "Bob", "Charlie"],
"age" => [25, 30, 35],
"salary" => [50000, 60000, 70000]
].unwrap();
println!("{}", df);
}
📌 실행 결과:
rust
복사
편집
shape: (3, 3)
┌─────────┬─────┬────────┐
│ name │ age │ salary │
│ --- │ --- │ --- │
│ str │ i32 │ i32 │
├─────────┼─────┼────────┤
│ Alice │ 25 │ 50000 │
│ Bob │ 30 │ 60000 │
│ Charlie │ 35 │ 70000 │
└─────────┴─────┴────────┘
✅ df![]를 사용해서 Pandas처럼 데이터 프레임을 만들 수 있어.
4️⃣ CSV 파일 불러오기
Polars는 CSV 파일을 빠르게 읽을 수 있어.
📌 example.csv 파일 만들기:
csv
복사
편집
name,age,salary
Alice,25,50000
Bob,30,60000
Charlie,35,70000
🛠 CSV 파일 읽기
rust
복사
편집
use polars::prelude::*;
fn main() -> PolarsResult<()> {
let df = CsvReader::from_path("example.csv")?
.infer_schema(None)
.has_header(true)
.finish()?;
println!("{}", df);
Ok(())
}
📌 실행 결과 (파일에서 데이터 불러오기):
rust
복사
편집
shape: (3, 3)
┌─────────┬─────┬────────┐
│ name │ age │ salary │
│ --- │ --- │ --- │
│ str │ i64 │ i64 │
├─────────┼─────┼────────┤
│ Alice │ 25 │ 50000 │
│ Bob │ 30 │ 60000 │
│ Charlie │ 35 │ 70000 │
└─────────┴─────┴────────┘
✅ CSV 파일을 데이터 프레임으로 변환 가능
5️⃣ 데이터 프레임 조작 (필터링, 정렬)
Polars에서는 데이터를 쉽게 조작할 수 있어.
🛠 특정 조건으로 필터링
rust
복사
편집
use polars::prelude::*;
fn main() -> PolarsResult<()> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "David"],
"age" => [25, 30, 35, 40],
"salary" => [50000, 60000, 70000, 80000]
]?;
let filtered_df = df.filter(&df["age"].gt(30)?)?;
println!("{}", filtered_df);
Ok(())
}
📌 실행 결과:
rust
복사
편집
shape: (2, 3)
┌─────────┬─────┬────────┐
│ name │ age │ salary │
│ --- │ --- │ --- │
│ str │ i32 │ i32 │
├─────────┼─────┼────────┤
│ Charlie │ 35 │ 70000 │
│ David │ 40 │ 80000 │
└─────────┴─────┴────────┘
✅ df.filter(&df["age"].gt(30)?) → 30보다 큰 나이 필터링
🛠 정렬 (Sorting)
rust
복사
편집
use polars::prelude::*;
fn main() -> PolarsResult<()> {
let df = df![
"name" => ["Alice", "Bob", "Charlie"],
"age" => [25, 30, 35],
"salary" => [50000, 70000, 60000]
]?;
let sorted_df = df.sort(["salary"], false)?; // 내림차순 정렬
println!("{}", sorted_df);
Ok(())
}
📌 실행 결과 (salary 기준 정렬):
rust
복사
편집
shape: (3, 3)
┌─────────┬─────┬────────┐
│ name │ age │ salary │
│ --- │ --- │ --- │
│ str │ i32 │ i32 │
├─────────┼─────┼────────┤
│ Bob │ 30 │ 70000 │
│ Charlie │ 35 │ 60000 │
│ Alice │ 25 │ 50000 │
└─────────┴─────┴────────┘
✅ .sort(["salary"], false)? → salary 기준 내림차순 정렬
6️⃣ Lazy Execution (지연 실행)
Polars는 Lazy Execution (지연 실행)을 지원해서 성능을 최적화할 수 있어.
즉, 필요한 연산만 실행해서 속도를 높여줘.
🛠 LazyFrame을 사용한 최적화
rust
복사
편집
use polars::prelude::*;
fn main() -> PolarsResult<()> {
let lf = LazyCsvReader::new("example.csv")
.has_header(true)
.finish()?
.filter(col("age").gt(lit(30))) // 30보다 큰 나이 필터링
.select([col("name"), col("salary")]); // 필요한 컬럼만 선택
let df = lf.collect()?; // 최적화된 실행
println!("{}", df);
Ok(())
}
✅ Lazy Execution을 사용하면 전체 데이터가 아니라 필요한 부분만 처리
🔹 Polars 정리
✅ df![] → 데이터 프레임 생성
✅ CsvReader::from_path() → CSV 파일 읽기
✅ .filter() → 조건 필터링
✅ .sort(["컬럼"], true/false) → 정렬
✅ LazyFrame → 최적화된 실행