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);
}

2025년 4월 14일 월요일

4-3-3강: 컬렉션(Binary Heap, String)

1. Binary Heap: 최대, 최소값을 찾을 때 많이 사용 

//최대값
let mut heap = BinaryHeap::new();
// 값 추가 (자동 정렬됨 - 최대 힙)
heap.push(10);
heap.push(5);
heap.push(20);

// 최대값 확인 (pop 없이)
if let Some(max) = heap.peek() {
    println!("최대값: {}", max); // 20
}

// 최대값 꺼내기 (pop은 값을 제거함)
if let Some(max) = heap.pop() {
    println!("꺼낸 값: {}", max); // 20
}

// 반복적으로 pop하면 큰 값부터 나옴
while let Some(val) = heap.pop() {
    println!("{}", val);
}

//최소 값
let mut min_heap = BinaryHeap::new();
min_heap.push(Reverse(10));
min_heap.push(Reverse(1));
min_heap.push(Reverse(5));

while let Some(Reverse(val)) = min_heap.pop() {
    println!("{}", val); // 1, 5, 10 순으로 출력
}


2. String : 문자열은 벡터를 사용해 문자를 관리


// String은 UTF-8로 인코딩된 문자열을 저장한다.
fn main() {
    // String은 String::new()로 생성할 수 있다.
    let mut eng = String::new();
    // String은 push_str() 메서드를 사용하여 문자열을 추가할 수 있다.
    eng.push_str("hello");

    // to_string() 메서드를 사용하여 &str을 String으로 변환할 수 있다.
    let jpn = "こんにちは".to_string();
    // String::from() 함수를 사용하여 &str을 String으로 변환할 수 있다.
    let kor = String::from("안녕하세요");

    println!("{} {} {}", eng, jpn, kor);
}
// format! 매크로로 포매팅된 문자열 만들수 있다.
fn main() {
    let str = String::from("안녕");
    let idx = 123;

    // format! 매크로는 문자열을 포매팅하여 새로운 String을 생성한다.
    // format! 매크로는 {}를 사용하여 포매팅할 위치를 지정한다.
    let s = format!("{} {}", str, idx);
    println!("{}", s);
}
// 문자열 내 문자들을 탐색하기
fn main() {
    let txt = String::from("안녕하세요.");

    // txt 내의 문자들을 순회한다.
    for c in txt.chars() {
        print!("{} ", c);
    }
}

4-3-2강: 컬렉션(HashMap, HashSet)

1. HashMap: 키-값 쌍 저장용

use std::collections::HashMap;

let mut scores = HashMap::new();

// 값 추가

scores.insert(String::from("Blue"), 10);

scores.insert(String::from("Red"), 20);

// 값 접근

if let Some(score) = scores.get("Blue") {

    println!("Blue 팀 점수: {}", score);

}

// 반복

for (key, value) in &scores {

    println!("{}: {}", key, value);

}

// 값 갱신

scores.insert(String::from("Blue"), 30);

// 키가 없을 때만 삽입

scores.entry(String::from("Green")).or_insert(50);


2. HashSet: 중복 없는 값 저장용.

let mut set = HashSet::new();

// 값 추가
set.insert("apple");
set.insert("banana");

// 포함 여부 확인
if set.contains("apple") {
    println!("사과 있음!");
}

// 값 제거
set.remove("banana");

// 반복
for item in &set {
    println!("{}", item);
}

4-3-1강: 컬렉션(Vec, LinkedList)

 1. 벡터 (Vec)

// 벡터 생성

let mut v: Vec<i32> = Vec::new();

let mut v = vec![1, 2, 3];

// 요소 추가

v.push(4);

// 요소 접근

let third = v[2];

match v.get(2) {

    Some(x) => println!("세 번째 요소: {}", x),

    None => println!("없음"),

}

// 요소 변경

v[1] = 20;

// 요소 삭제

v.remove(0); // 0번 인덱스 삭제

v.pop();     // 마지막 요소 삭제


2. 링크드 리스트 (LinkedList)

use std::collections::LinkedList;

let mut list = LinkedList::new();

// 요소 추가
list.push_back(10); // 뒤에 추가
list.push_front(5); // 앞에 추가

// 요소 삭제
list.pop_back();
list.pop_front();

// 반복 및 접근
for val in &list {
    println!("{}", val);
}

  • Vec은 인덱스로 바로 접근 가능하며, 대부분의 경우 LinkedList보다 빠르고 유리함.

  • LinkedList는 요소 삽입/삭제가 많고 중간 삽입이 필요한 경우 적합.