2025년 4월 18일 금요일

5-3-3강: SQLite 사용하기

 SQLite 를 사용하는 간단한 예제

/* SQLite 의존성 추가
[dependencies]
sqlite = "0.30" */
use sqlite;
use sqlite::State;

fn main() {
    // 메모리에 sqlite db 생성
    let connection = sqlite::open(":memory:").unwrap();

    // users 테이블 만들고, 2개 데이터 삽입
    let query = "
        CREATE TABLE users (name TEXT, age INTEGER);
        INSERT INTO users VALUES ('루나', 3);
        INSERT INTO users VALUES ('러스트', 13);
    ";
    // 테이블 생성 쿼리를 실행
    connection.execute(query).unwrap();

    // ?는 나중에 이 자리에 값을 넣을꺼야 표시
    // SQL 인젝션을 막기 위해 사용
    let query = "SELECT * FROM users WHERE age > ?";
   
    // 쿼리를 실행
    let mut statement = connection.prepare(query).unwrap();

    // (1, 5) → 1은 첫 번째 파라미터 위치 (?)를 의미
    // 5는 ?에 들어갈 값
    statement.bind((1, 5)).unwrap(); // age > 5
   
    // 테이블의 데이터를 조회
    while let Ok(State::Row) = statement.next() {
        println!("name = {}", statement.read::<String, _>("name").unwrap());
        println!("age = {}", statement.read::<i64, _>("age").unwrap());
    }
}

/*실행결과
name = 러스트
age = 13 */

5-3-2강: 파일 입출력(데이터 버퍼링, 데이터 직렬화)

 1. 데이터 버퍼링이란?

데이터를 주고 받는 동안 일시적으로 데이터를 버퍼에 저장하는 것

// 데이터 버퍼링 예제
use std::fs::File;
use std::io::{BufRead, BufReader};

/*input.txt 내용
hello world
i am ferris!
haha...
*/

fn main() {
    let file = File::open("input.txt").unwrap();
    // BufReader 생성
    let reader = BufReader::new(file);

    // File 을 읽는다.
    for line in reader.lines() {
        let line = line.unwrap();
        println!("{}", line);
    }
}
/*실행결과
hello world
i am ferris!
haha...  */

2.데이터 직렬화란?

자료의 상태를 저장하기 위해 바이트 스트림으로 변환하는 것

직렬화된 데이터는  파일, 네트워크, 메모리에 저장할 수 있다.

반대로 원래의 형태로 복원하는 것을 역 직렬화 라고 한다.

예제는 Serde 를 사용해서 json 파일로 저장하고 복원한다.

// 데이터 직렬화 예제
/* serde, serde_json 를 사용하려면 의존성 추가 해야 한다.
[dependencies]
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
*/

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let pt = Point { x: 10, y: 20 };
    //pt 를 json 형식으로 변환한다.
    let json = serde_json::to_string(&pt).unwrap();
    println!("json: {}", json);

    // json을 사용해서 Point를 생성한다.
    let pt: Point = serde_json::from_str(&json).unwrap();
    println!("point: [{}, {}]", pt.x, pt.y);
}

/*실행결과
json: {"x":10,"y":20}
point: [10, 20] */

5-3-1강: 파일 입출력(동기식, 비동기식)

 1. 동기식 파일 입출력 이란?

동기식 입출력 방식은 작업이 완료될 때 까지 무한정 기다린다.

입출력이 완료될 때까지 기다리니 CPU 자원을 비효율적으로 사용한다.

// 동기식 입출력 방식의 예제
use std::fs::File;
use std::io::{Read, Write};

fn main() {
    let mut file = File::open("input.txt").unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
    // 파일을 읽을때 까지 대기합니다.

    println!("{}", contents);

    let mut file = File::create("output.txt").unwrap();
    file.write_all(contents.as_bytes()).unwrap();
    // 파일을 쓸때 까지 대기합니다.
}

2. 비동기식 파일 입출력이란?
비동기식 입출력 방식은 메인 스레드를 멈추지 않기에
CPU 자원을 효율적으로 사용한다.

/*cargo.toml에 의존성 추가
[dependencies]
tokio = { version = "1.25.0", features = ["full"] } */
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() {
    // 비동기 방식으로 file 핸들러를 얻는다.
    //input.txt 엔 hello world 가 입력되어 있다.
    let mut file = File::open("input.txt").await.unwrap();
    let mut contents = String::new();

    // 비동기 방식으로 file 읽기
    file.read_to_string(&mut contents).await.unwrap();

    // input.txt의 내용을 출력
    println!("{}", contents);

    //비동기 방식으로 file 생성
    let mut file = File::create("output.txt").await.unwrap();
    //비동기 방식으로 file 저장 (input.txt에 있는 hello world가 output.txt에 저장된다.)
    file.write_all(contents.as_bytes()).await.unwrap();
}

// 비동기 방식으로 구현한 이벤트 루프 예제
/* 의존성 추가
[dependencies]
tokio = { version = "1.25.0", features = ["full"] }
*/
use tokio::io::{stdin, BufReader, AsyncBufReadExt};
use tokio::fs::File;

#[tokio::main]
async fn main() {
    let mut reader = BufReader::new(stdin());
    let mut lines = reader.lines();

    loop { // quit가 입력될때 까지 입력을 받음
        match lines.next_line().await.unwrap() {
            Some(input) => {
                println!("입력: {}", input);
       
                if input == "quit" {
                    break;
                }
            }
            None => {
                break;
            },
        };
    }
}

2025년 4월 17일 목요일

5-2-2강: 동시성 제어 기법(Semaphore)

세마포어란? 

복수의 제한된 자원에 다수의 스레드가 동시에 접근하는 것을 막는 동시성 제어 방법

여러 스레드가 동시에 세마포어에 접근할 수 있도록 Arc<T> 를 사용한다.

세마포어는 임계 지정을 직접 지정해야 한다.

세마포어를 사용하려면 tokio 크레이트를 사용해야 한다.

cargo.toml 에 다음과 같이 의존성 추가 한다.

[dependencies]

tokio = { version = "1.25.0", features = ["full"] }

use std::sync::{Arc, Mutex};
use tokio::sync::Semaphore;

// 공유 카운터를 위한 뮤텍스
static counter: Mutex<i32> = Mutex::new(0);

#[tokio::main]
async fn main() {
    // 동시에 2개의 thread가 접근 가능하도록 세마포어 설정
    let semaphore = Arc::new(Semaphore::new(2));
    let mut future_vec = vec![];

    for _ in 0..100 {
        // semaphore 획득
        let permit = semaphore.clone().acquire_owned().await.unwrap();
        let future = tokio::spawn(async move {
            //뮤텍스로부터 안전한 참조 획득    
            let mut num = counter.lock().unwrap();
            *num = *num + 1; //카운터 증가

            drop(permit); // semaphore 해제
        });
        future_vec.push(future); //생성된 future를 벡터에 저장
    }

    for future in future_vec {
        future.await.unwrap(); //모든 future가 완료될 때까지 대기
    }

    println!("결과: {}", *counter.lock().unwrap()); //최종 결과 출력
}

/*실행결과
결과: 100
 */

5-2-1강: 동시성 제어 기법(Mutex)

 Rust에서 스레드와 Mutex로 전역 변수 안전하게 공유하기


Rust에서는 여러 스레드가 동시에 데이터를 다룰 경우, **경쟁 상태(Race Condition)**를 피하기 위해 Mutex를 자주 사용합니다. 
아래 예제는 100개의 스레드가 하나의 전역 변수 counter를 안전하게 1씩 증가시키는 코드입니다.

use std::thread;
use std::sync::Mutex;

//Mutex 는 여러 스레드가 공유자원에 동시에 접근하지 못하도록 막는 기법
//Mutex 는 잠금(lock)와 해제(unlock)의 두 가지 상태가 존재
static counter: Mutex<i32> = Mutex::new(0); // counter를 전역변수로 정의

fn inc_counter() {
    // lock을 걸고 접근 권한을 획득함
    //unwrap()은 lock 실패 시 패닉을 발생시킴.
    let mut num = counter.lock().unwrap();
    *num = *num + 1; // 자원에 접근하려면 *키워드를 사용한다.
} // inc_counter를 벗어나는 순간 counter는 unlock됩니다.

fn main() {
    let mut thread_vec = vec![];

    // _ 는 변수 이름이 정의되어야 할 부분에서 변수명을 생략할 때 사용한다.
    for _ in 0..100 {
        let th = thread::spawn(inc_counter);
        thread_vec.push(th);
    }

    //각 스레드가 끝날 때까지 기다림.
    for th in thread_vec {
        //join()을 하지 않으면 메인 함수가 먼저 끝날 수도 있음.
        th.join().unwrap();
    }

    // 마지막으로 counter의 값을 lock으로 가져와 출력.
    println!("결과: {}", *counter.lock().unwrap());
}

/*실행결과
결과: 100
*/

핵심 포인트

  • Mutex<i32>를 사용해 여러 스레드가 동시에 안전하게 공유 자원에 접근 가능

  • lock().unwrap()으로 lock을 획득하고, 해제는 스코프 종료 시 자동

  • join()으로 메인 스레드가 하위 스레드를 기다림

왜 Mutex가 필요한가?

스레드가 동시에 값을 수정하면 예상치 못한 오류가 발생할 수 있습니다. 

Mutex는 이런 충돌을 방지하는 락(lock) 메커니즘을 제공합니다.

2025년 4월 16일 수요일

5-1-2: 동시성 중 async/await, tokio

async/await : 비동기 프로그래밍을 할 수 있는 기법 제공
async/await 를 사용하려면 Cargo.toml 파일에 추가
[dependencies]
futures = "0.3"

// await 를 사용해 다른 async 함수 호출 예제
use futures::executor::block_on;

//비동기 함수 정의
async fn calc_sum(start: i32, end: i32) -> i32 {
    let mut sum = 0;

    for i in start..=end {
        sum += i;
    }

    sum
}
//비동기 함수 정의
async fn calc() -> i32 {
    let sum1_50 = calc_sum(1, 50).await; //await 키워드롤 결과 얻기
    let sum51_100 = calc_sum(51, 100).await; //await 키워드롤 결과 얻기
    let ret = sum1_50 + sum51_100;

    ret
}

fn main() {
    let future = calc();

    // block_on() 실행 calc가 종료될 때까지 메인 스레드는 멈춘다.
    let sum = block_on(future);
    println!("1부터 100까지의 합: {}", sum);
}

tokio : 비동기 작업을 실행할 수 있도록 도와주는 런타임 라이브러리
Cargo.toml 에 의존성 추가
[dependencies]
tokio = { version = "1.25.0", features = ["full"] }
use std::time::Duration;
use tokio::time;

// async 로 비동기 함수로 지정
async fn sleep_10sec() {
    for i in 1..10 {
        println!(".");
        // 1초간 10회 대기
        time::sleep(Duration::from_millis(1000)).await;
    }
}

// async 로 비동기 함수로 지정
async fn calc_sum(start: i32, end: i32) -> i32 {
    let mut sum = 0;

    for i in start..=end {
        println!("{} ", i);
        sum += i;
    }

    sum
}

async fn calc() -> i32 {
    let f1 = sleep_10sec();
    let f2 = calc_sum(1, 10);

    // sleep_10sec() 와 calc_sum()가 동시에 수행된다.
    //tokio::join!() 를 사용해 비동기 함수를 대기한다.
    let (_, sum) = tokio::join!(f1, f2);
    sum
}

//tokio를 사용하는 비동기 메인 함수
#[tokio::main]
async fn main() {
    let sum = calc().await;

    println!("sum={}", sum);
}

5-1-1강: 동시성 중 thread, mpsc

동시성 - 시스템이 여러 작업을 동시에 실행되는 것처럼 보이게 하는 것

병렬성 - 여러 작업을 실제로 동시에 수행하는 것

thread : 스레드를 생성하고 제어
use std::fs::File;
use std::io::{BufReader, BufRead};
use std::thread;

fn main() {
    // 새로운 스레드를 생성하고, 그 핸들을 받기
    let handle = thread::spawn(|| {
        //"file.txt" 파일 열기
        let file = File::open("file.txt").unwrap();

        // 버퍼링을 사용해 파일 읽기
        let reader = BufReader::new(file);

        // 파일의 각 줄을 읽어오기
        for line in reader.lines() {
            // 각 줄의 텍스트 읽기
            let txt = line.unwrap();
            println!("{}", txt);
        }
    });

    // 스레드가 끝날 때까지 대기
    // 스레드가 종료되면 join() 메서드가 호출됨
    match handle.join() {
        Ok(_) => {},
        Err(e) => {
            println!("스레드 내부에서 오류가 발생했습니다. {:?}", e);
        }
    };
}
mpsc : 채널을 생성하여 여러 스레드의 데이터를 공유
fn main() {
    //mpsc 채널 생성 tx는 송신자, rx는 수신자
    let (tx1, rx) = mpsc::channel();
    let tx2 = mpsc::Sender::clone(&tx1); // tx1복제

    // 1부터 50까지의 합
    thread::spawn(move || {
        let mut sum = 0;

        for i in 1..=50 {
            sum = sum + i;
        }

        tx1.send(sum).unwrap();
    });

    // 51부터 100까지의 합
    thread::spawn(move || {
        let mut sum = 0;

        for i in 51..=100 {
            sum = sum + i;
        }

        tx2.send(sum).unwrap();
    });

    let mut sum = 0;
   
    for val in rx {
        println!("수신: {}", val);
        sum = sum + val;
    }

    println!("1부터 100까지의 합: {}", sum);
}