액터 모델과 동시성 제어

notJoon·2023년 6월 29일
1

액터 모델

목록 보기
1/3

소개

액터 모델(Actor model)은 동시성 프로그래밍의 복잡성을 관리하기 위한 도구 중 하나입니다. 이번 시리즈에서는 액터 모델의 기본 개념과 구현 그리고 동시성 제어 각각 두 가지 내용을 다룰 것입니다. 본격적인 구현에 앞서 먼저 러스트의 대표적인 동시성 제어 타입에 대해 알아보겠습니다.

동시성 제어

액터 모델을 구현하기 전에 먼저 러스트의 동시성 제어에 대해 알아보겠습니다. 동시성 제어는 여러 프로세스 또는 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 방지하고, 공유 자원에 대한 일관성과 무결성을 유지하는 메커니즘입니다. 병렬 프로그래밍의 중요한 요소이며, 프로그램의 실행 순서에 따라 결과가 달라질 수 있는 비결정성을 관리하는게 중요합니다.

여러 스레드가 데이터에 접근할 때 발생하는 문제 중 대표적인 것은 경쟁 상태와 데드락입니다.

경쟁 상태(race condition)는 여러 프로세스가 동시에 공유하는 자원(데이터)에 접근할 때 일어날 때 발생하는 문제입니다. 예를 들어, 두 개의 스레드 A, B가 있다고 가정해보겠습니다. 만약 A가 데이터를 읽고 업데이트하는 도중에 B가 그 데이터를 변경하면, A가 하고 있던 작업은 중지되거나 예상치 못한 결과를 일으킬 수 있습니다. 참고로 경합 상태를 일으키는 부분을 크리티컬 섹션(critical section, 위험 영역)이라고 부릅니다. 이 영역을 보호하기 위해 이후에 설명할 동기 처리를 이용합니다.

또한 데드락은 여러 프로세스나 스레드가 서로가 가진 자원을 점유하고 있는 상태에서, 서로가 점유한 자원을 기다리며 무한히 대기하는 상황을 말합니다. 이때 프로그램은 더 이상 진행할 수 없게 되며, 데드락 상태에 있는 프로세스나 스레드는 해결되기 전까지 계속 대기하게 됩니다.

이러한 문제를 미연에 방지하기 위해서 동시성 매커니즘을 사용하는데, 러스트에는 Mutex, Arc, Condvar, RwLock 등의 도구가 있습니다. 이 도구들은 스레드 간의 실행 순서를 조정하고, 데이터의 접근을 제어하는 등의 역할을 하여 동시성 문제를 관리합니다.

Mutex와 RwLock

뮤텍스(Mutex)는 상호 배제(Mutual Exclusion)의 줄임말로, 가장 자주 사용하는 동기화 기법입니다. 동시에 데이터에 접근하려는 다른 스레드를 일시적으로 차단하여 특정 스레드가 일부 데이터에 독점적으로 접근할 수 있도록 하는 역할을 합니다. 다른 표현으로는 크리티컬 섹션을 실행할 수 있는 프로세스의 수를 최대 1개로 제한하는 동기 처리입니다.

예를 들어, 크리티컬 섹션을 처리하는 플래그를 생각할 수 있습니다. 해당 플래그가 참이면 작업을 실행하고, 그렇지 않으면 실행하지 않는 동작을 생각해볼 수 있습니다. 이 구역에 대한 접근 권한을 제어하는 것을 잠금(locking)이라고 부르며, 이는 잠금(lock)과 해제(unlock)의 두 가지 형태로 이루어집니다.

  1. 락(Lock): 데이터에 대한 독점적인 접근을 획득하기 위해 사용됩니다. 락을 획득한 스레드는 데이터를 읽거나 수정할 수 있습니다. 락을 얻지 못한 상태에서 데이터에 접근하는 것은 불가능 하고, 락을 획득할 때 까지 스레드는 대기해야 합니다.
  2. 언락(unlock): 락을 반환하여 다른 스레드가 접근할 수 있도록 합니다. 데이터 접근이나 수정이 완료된 이후에는 락을 반환하여 다른 스레드가 데이터에 접근 할 수 있도록 해야 합니다.

뮤텍스를 사용해 스레드가 락을 획득하고 반환하는 예시 코드입니다.

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

fn main() {
	let data = Mutex::new(0);

	let thread1 = thread::spwan(move || {
		let mut guard = data.lock().unwrap(); // lock 획득
		
		// lock을 획득한 스레드는 데이터 수정 가능
		*guard += 1;
		println!("thread 1 value: {}". *guard);

		/* 스코프 범위를 벗어나면 자동으로 unlock */
	});

	let thread2 = thread::spwan(move || {
		let mut guard = data.lock().unwrap();
		
		*guard += 10;
		println!("thread 1 value: {}". *guard);

		drop(guard); // 명시적인 unlock도 가능합니다
	});

	// `join()`을 호출해 스레드의 작업이 완료될때 까지 대기
	thread1.join().unwrap();
	thread2.join().unwrap();	
}

러스트에서는 뮤텍스로 보호되는 변수는 데이터를 보존하기 위해 락 없이는 접근할 수 없도록 되어 있습니다. 또한, 보호 대상 데이터가 스코프를 벗어나면 락이 자동으로 해제됩니다. lock 함수는 LockResult<MutexGuard<'_, T>>라는 타입을 반환하는데, LockResult는 다음과 같이 정의됩니다.

type LockResult<Guard> = Result<Guard, poisonError<Guard>>;

lock 함수를 사용하여 락을 획득하는 경우, MutexGuard라는 타입으로 보호 대상 데이터를 감싸서 반환하며, 이 MutexGuard 변수의 스코프를 벗어나면 락이 자동으로 해제됩니다. 그래서 명시적으로 해제를 하지 않아도 됩니다. 또한, 어떤 스레드가 락 획득 중에 패닉에 빠진다면 해당 뮤텍스는 poisoned 상태에 있는 것으로 간주되고 락 획득에 실합니다.

RwLock은 읽기-쓰기 잠금(read-write lock)의 약자입니다. RwLock은 동시에 여러 개의 읽기 작업은 허용하지만, 쓰기 작업은 한 번에 하나의 스레드만 수행할 수 있도록 하는 동기화 기법입니다. 왜냐하면 쓰기 작업과 다르게 읽는 작업은 데이터를 수정할 필요가 없기 때문입니다.

RefCell 처럼 RwLock은 내부에서 참조 카운팅을 통해 공유 borrow와 독점 borrow의 수를 추적합니다. 하지만, RwLock은 충돌 borrow가 발생해도 패닉을 일으키는 대신, 현재 스레드를 block 상태로 변경하고, 문제가 되는 borrow가 사라질 때 까지 대기합니다.

뮤텍스 처럼 RwLock의 내용을 borrow하는 것을 locking이라고 합니다. 하지만 RwLock의 경우, 읽기 작업과 쓰기 작업 간의 동시성을 관리하기 위해 두 가지 종류의 락을 사용합니다.

  1. 읽기 락(Read Lock): 읽기 락은 데이터를 공유하는 경우에 사용합니다. 여러 스레드가 동시에 읽기 락을 얻을 수 있으며, 쓰기 작업과 상호 배제되지 않으므로 여러 스레드가 동시에 읽기 작업을 할 수 있습니다.
  2. 쓰기 락(Write Lock): 쓰기 락은 데이터 수정 시 사용됩니다. 쓰기 락은 읽기 락과 상호 배제되며, 한 번에 하나의 스레드만 쓰기 락을 얻을 수 있습니다. 쓰기 락을 얻은 스레드는 데이터를 수정하고, 다른 스레드들은 읽기 락이나 쓰기 락을 얻을 때까지 대기해야 합니다.

다음은 RwLock을 사용하는 예입니다.

use std::sync::RwLock;
use std::thread;
use std::time::Duration;

fn main() {
    let data = RwLock::new(0); // RwLock으로 보호되는 데이터

    // 읽기 작업을 수행하는 스레드
    for _ in 0..5 {
        let data = data.clone(); // RwLock을 클론하여 여러 스레드가 동시에 읽을 수 있도록 함

        thread::spawn(move || {
            // 읽기 락을 얻음
            let guard = data.read().unwrap();
            println!("read: {}", *guard);
        });
    }

    // 쓰기 작업을 수행하는 스레드
    for _ in 0..3 {
        let data = data.clone(); // RwLock을 클론하여 여러 스레드가 동시에 쓸 수 있도록 함

        thread::spawn(move || {
            // 쓰기 락을 얻음
            let mut guard = data.write().unwrap();
            *guard += 1; // 데이터 수정
            println!("write: {}", *guard);
        });
    }

    thread::sleep(Duration::from_secs(2)); // 스레드들이 작업을 완료할 때까지 대기
}

RwLock을 이용해 여러 스레드가 동시에 데이터에 접근하는 예제입니다. 읽기 작업을 수행하는 스레드는 read() 메서드를 호출하여 읽기 락을 얻고, 쓰기 작업을 수행하는 스레드는 write() 메서드를 호출하여 쓰기 락을 얻습니다.

read()를 호출하면 보호 대상 데이터를 RwLockreadGuard 타입으로 감싼 불변 참조를 얻을 수 있고, 읽는 것만 가능합니다. 또한 뮤텍스와 같이 해당 참조의 스코프를 벗어나면 자동으로 락이 해제됩니다.

write()의 경우에도 마찬가지입니다. 이 함수를 호출하면 보호 대상 데이터를 RwLockWriteGuard 타입으로 감싼 가변 참조를 얻을 수 있습니다.

Arc(Atomic Reference Counting)

Arc는 여러 스레드에서 공유할 수 있는 객체에 대한 참조를 나타내고, 하나의 데이터에 대한 여러 소유권을 안전하게 관리하는 동시성 타입입니다.

데이터에 대한 참조 횟수를 추적하며, 스레드가 Arc를 통해 데이터에 접근하면 참조 카운터가 증가하고 참조가 끝나면 감소합니다. 이후에 참조 카운터가 0이 되면, 데이터는 해제됩니다. 따라서 여러 스레드가 동시에 데이터에 접근해도 참조 카운터를 조작하면서 데이터에 대한 소유권 충돌을 피할 수 있습니다.

Rc 역시 참조 횟수를 추적하지만, Arc와 비교하면 공유의 측면에서 차이가 있습니다. ArcRc와 달리 참조 카운팅을 원자적 연산을 사용하여 관리하므로 여러 스레드 간에 안전하게 공유할 수 있습니다. 이를 통해 여러 스레드가 동시에 Arc를 통해 데이터에 접근하고 참조 카운터를 조작할 수 있습니다. 반면에 Rc는 단일 스레드에서만 사용하기에 적합하며, 스레드 간에 공유할 때는 안전하지 않습니다.

또한, Arc는 참조 카운팅을 관리하기 위해 원자적 연산을 사용하는데 이 경우 일반적인 메모리 액세스보다 더 많은 비용이 발생할 수 있습니다.

ArcSendSync 트레이트를 구현한 타입에 대해서만 SendSync를 자동으로 구현합니다. 하지만 Arc<T>에 스레드 안전하지 않은 타입 T를 넣는 것은 불가능합니다. 이는 Arc가 참조 카운팅을 관리하는 데 있어서 안전하지만, 내부 데이터에 대해서는 스레드 안전성을 추가하지 않기 때문입니다. 따라서 스레드 안전하지 않은 타입을 Arc<T> 안에 넣어 스레드 안전하게 만들 수 없습니다. 이런 경우에는 Arc<T>와 함께 std::sync 타입인 Mutex<T>를 사용하여 스레드 안전성을 보장할 수 있습니다.

다음은 Arc를 사용해 여러 스레드가 공유 데이터에 접근하는 코드입니다.

use std::sync::Arc;
use std::thread;
use std::time::Duration;

fn main() {
	let data = Arc::new(0);

	for _ in 0..5 {
		let cloned = Arc::clone(data); // `Arc`를 clone하여 스레드마다 공유

		thread::spwan(move || {
			println!(
				"thread: {}, value: {}", 
				thread::current().id(), 
				*data,
			);
		});		
	}

	// 스레드들이 실행을 완료할 때까지 대기
	thread::sleep(Duration::from_secs(1));
}

위 예시에서는 Arc를 사용하여 여러 스레드에서 데이터에 접근하고 출력합니다. Arc::new()를 사용하여 데이터를 Arc로 감싸고, 스레드마다 Arc를 클론하여 공유합니다. 각 스레드는 공유된 Arc를 통해 데이터에 접근할 수 있습니다. Arc는 참조 카운팅을 관리하므로 여러 스레드가 동시에 데이터에 접근해도 안전하게 참조 카운터를 조작하여 데이터에 대한 소유권 충돌을 방지합니다.

요약하자면, Arc는 여러 스레드 간에 공유할 수 있는 참조 타입으로, 참조 카운팅을 원자적 연산으로 관리하여 스레드 안전하게 공유할 수 있습니다. 그러나 Rc와 비교하면 약간의 오버헤드가 발생할 수 있습니다. ArcSendSync를 구현한 타입에 대해서만 자동으로 구현되며, 스레드 안전하지 않은 타입을 Arc<T> 안에 넣을 수는 없습니다. 이런 경우에는 Arc<T>와 함께 Mutex<T> 등의 std::sync 타입을 사용하여 스레드 안전성을 보장할 수 있습니다.

Condvar(Condition Variables)

Condvar는 스레드 간의 조건부 동기화를 위한 메커니즘입니다. Condvar는 특정 조건이 만족될 때 까지 스레드를 대기 상태로 만듭니다.

주로 뮤텍스와 함께 사용되며, 뮤텍스로 보호되는 데이터의 상태에 따라 스레드 동작을 조절합니다. 다음은 Condvar를 활용한 예제입니다.

use std::sync::{Arc, Mutex, Condvar};
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(false)); // 뮤텍스로 보호되는 데이터
    let condvar = Arc::new(Condvar::new()); // 조건 변수

    // 스레드 1
    let data_clone = Arc::clone(&data);
    let condvar_clone = Arc::clone(&condvar);
    let thread1 = thread::spawn(move || {
        let mut guard = data_clone.lock().unwrap(); // 뮤텍스 락 획득

        // 특정 조건이 충족되지 않으면 대기
        while !*guard {
            guard = condvar_clone.wait(guard).unwrap();
        }

        // 조건이 충족되면 작업 수행
        println!("threa 1: Pass");
    });

    // 스레드 2
    let data_clone = Arc::clone(&data);
    let condvar_clone = Arc::clone(&condvar);
    let thread2 = thread::spawn(move || {
        let mut guard = data_clone.lock().unwrap(); // 뮤텍스 락 획득

        // 특정 작업 수행 후 조건 충족
        *guard = true;

        // 대기 중인 스레드를 깨움
        condvar_clone.notify_one();

        // 뮤텍스 락은 여전히 보유한 상태로 블록을 벗어남
        println!("thread 2: Pass");
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
}

Condvar에는 스레드 간의 조건부 동기화를 위해 사용되는 메서드들이 있습니다. wait(), notify_one() 그리고 notify_all() 들인데요, 이 메서드들은 주로 공유 데이터의 특정 상태가 변경되기를 기다리는 작업에서 주로 사용됩니다. 대기 중인 스레드는 조건이 충족되기 전까지 작업을 중지하고, 조건이 충족되면 알림을 받아 작업을 재개합니다.

  1. wait() : 특정 조건이 충족될 때까지 스레드를 대기 상태로 만듭니다. 뮤텍스의 락을 획득한 상태에서 wait()을 호출하면 해당 스레드는 뮤텍스를 언락하고 대기 상태로 전환됩니다. 대기 중인 스레드는 다른
    스레드에 의해 깨어날 때까지 대기하다가, 깨어나면 다시 뮤텍스의 락을 획득하고 작업을 재개합니다.
  2. notify_one(): 대기 중인 메서드 하나를 깨우고, 해당 스레드가 뮤텍스의 락을 획득하게 해주는 역할을 합니다. 여러 스레드가 대기 중이더라도, notify_one()은 호출 될 때마다 대기 중인 스레드 중 하나만 깨웁니다. 다만, 어떤 스레드가 활성화 될지는 모릅니다.
  3. notify_all(): 대기 중인 모든 스레드를 깨웁니다. 대기 중인 스레드가 없는 경우 아무 것도 하지 않습니다.

정리

  1. Mutex는 상호 배제를 구현하기 위한 타입으로, 데이터에 동시에 접근하는 것을 제어합니다. Mutexlock()unlock()을 통해 데이터의 독점적인 접근을 관리합니다.
  2. RwLock은 읽기와 쓰기 작업 간의 동시성을 관리하는 타입입니다. 여러 스레드가 동시에 읽기 작업을 수행할 수 있지만, 쓰기 작업은 한 번에 하나의 스레드만 수행할 수 있도록 제한합니다.
  3. Arc는 여러 스레드 간에 안전하게 공유할 수 있는 참조 카운팅 타입입니다. Arc는 여러 소유권을 안전하게 관리하고, 데이터에 대한 참조 횟수를 추적하여 스레드 간의 안전한 공유를 가능하게 합니다.
  4. Condvar는 스레드 간의 조건부 동기화를 위한 메커니즘입니다. Condvar는 특정 조건이 충족될 때까지 스레드를 대기 상태로 만들고, 조건이 충족되면 스레드를 깨워 작업을 진행할 수 있도록 합니다.

참고 자료

  1. Atomics and Locks (Mara Bos, 2023, O'Reilly, p.15-24)
  2. 동시성 프로그래밍 (다카노 유키, 역: 김모세, 2022, 한빛미디어, p.83-116)
  3. https://doc.rust-lang.org/std/sync/struct.Arc.html
profile
Uncertified Quasi-polyglot pseudo dev

1개의 댓글

comment-user-thumbnail
2023년 7월 2일

https://melonplaymods.com/2023/06/10/huggy-wuggy-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/call-of-duty-character-2-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/weapon-shotgun-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/soviet-soldiernpc-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/bleachsignusik-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/mk-1-mark-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/three-knives-and-a-gun-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/melonpiece-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/aircraft-carrier-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/t90-a-tank-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/berserkfrede22-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/scp-049-2-g-o-c-soldiernpc-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/dark-background-with-stars-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/torpedo-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/aki-hayakawa-or-fox-devil-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/dragon-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/street-lights-lighting-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/door-roblox-hotel-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/low-fire-guns-and-shells-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/mutant-banban-mod-for-melon-playground/

답글 달기