ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Go언어 시작하기 - Go developer를 위한 Rust 패러다임 / Paradigms of RUST for GO developers
    Media_Dev 2021. 1. 9. 12:39
    반응형

    이걸 보는 분들은 오랫동안 go 언어의 빅팬일 가능성이 높을 것 같아요. 그만큼 엄청난 변화를 가져온 랭귀지이죠. 특히 클라우드에서 고 언어는 아주 탁월하지 않은가요? 대체 언어가 없다는 개인적인 의견입니다. 사실 경량화 쓰레드는 엄청난량의 데이터를 처리하고도 남죠. 

    하지만 한편으로 여러분은 Rust도 하고 싶을 거예요. 저는 그 마음을 충분히 이해합니다. 

    그럼 이제부터 우리의 열망대로 이 언어로 아까운 시간을 태워볼까요. 그럴만한 가치가 있긴 하죠. 왜냐하면.. 음.. X소리는 그만두고 바로 들어가 봅시다.

    너무 복잡하다 이거 왜써? 맞다 복잡하다 보기에도 혹자는 그런 얘기를 하죠. "네가 RUST코드 할 때 시간을 들이면 런타임 시 10배의 보증을 받을 수 있다" 

    func main() {
    	c := make(chan bool)
    	m := make(map[string]string)
    	go func() {
    		m["1"] = "a" // 충돌지점 
    		c <- true
    	}()
    	m["2"] = "b" // 출돌지점
    	<-c
    	for k, v := range m {
    		fmt.Println(k, v)
    	}
    }
    

     

    위에 코드를 보면 채널 m은 동시에 mutated 됨을 알 수 있어요. 즉 동기화 이슈가 생기는거죠. 문제는 GO의 reace detactor는 이를 통과시킨다는 겁니다.

    그냥  <-C의 순서를 간단히 바꿔주면 끝나는 일이긴 합니다. dectector 가 빌드시켰다는 걸 보여주고 싶은 거죠.

    비슷하게 RUST로 code 해보겠습니다.

    use std::sync::mpsc::channel;
    use std::collections::HashMap;
    use std::thread;
    
    fn main() {
        let (c_tx, c_rx) = channel();
        let mut m = HashMap::new();
    
        thread::spawn(move || {
            m.insert("1", "a");
            c_tx.send(true).unwrap();
        });
    
        m.insert("2", "b");
        c_rx.recv().unwrap();
    
        for (k, v) in &m {
            println!("{}, {}", k, v);
        }
    }
    

    rust playground

    좀 달라 보이긴 하는데 일단 GO와 동일한 채널이 등장합니다!!!


    전송은 c_tx , receiving 부분은 c_rx 이 되겠습니다. Rust는 GO처럼 경량화 쓰레딩 모델을 사용하지 않았아요. thread::spawn는 Native OS thread와 1:1 맵핑됩니다. 

    문제는 위의 코드는 빌드되지 않는다는거예요. 

       Compiling playground v0.0.1 (/playground)
    error[E0382]: borrow of moved value: `m`
      --> src/main.rs:14:5
       |
    7  |     let mut m = HashMap::new();
       |         ----- move occurs because `m` has type `HashMap<&str, &str>`, which does not implement the `Copy` trait
    8  | 
    9  |     thread::spawn(move || {
       |                   ------- value moved into closure here
    10 |         m.insert("1", "a");
       |         - variable moved due to use in closure
    ...
    14 |     m.insert("2", "b");
       |     ^ value borrowed here after move
    
    error: aborting due to previous error
    
    For more information about this error, try `rustc --explain E0382`.
    error: could not compile `playground`
    
    To learn more, run the command again with --verbose.

    Rust HashMap은 Ownership 이 spowned 된 thread로 이동되었다고 말하고 있어요.
    더 이상 m 변수는 사용할 수 없고 위에서 우리가 얘기한 데이터 레이스와는 다른 관점으로 데이터 Race라고 Check 하고 있네요  

    아래의 내용을 기억하자 
    - 데이터의 오너십은 한 명이다. 
    - 오너십은 이동될 수 있다. 
    - 소유 값은 일시적으로 borrow 할 수 있다. (이하 벌로우라고 쓸게요.)
    - 문제가 발생 시 위에 방법은 move를 방지할 수 있다. 

    자 다시 데이터 레이스는 뭔가요?
    - 두 개 이상의 스레드가 같은 데이터를 읽는 것 
    - 그중 하나가 동기화를 제대로 사용 안 한 것 
    - 그중 하나가 쓰고 있을 때 

    RUST는 오너십과 일시적인 borrow를 통해서 데이터 레이스가 런타임에 방생하는 것을 막고 이런 형태의 코드는 Race Detector에서 잡아 낼 수 있음을 확인할 수 있어요. 쓰래드 경쟁에 대한 테스트 코드도 우리가 간혹 uint test로 만들어내는데 이런 수고에서 일정 부분 해방될 수 있을 것 같네요

    또한 러스트의 ownerhip과 lifetime 모델은 여러분의 신나는 코딩에서 귀찮은 룰을 만들긴 하지만 프로덕션에는 생기는 문제를 미연에 방지할 수 있다는 장점이 있어요. 최소 빌드가 된다면 서버가 세그먼트 폴트나 데이터 레이스로 인한 이상 동작은 최대한 줄일 수 있다는 이야기죠.

    그럼 좀 다른 방법으로 data를 공유하는 형태로 코드 해보죠. Shared memory를 사용하는 방법입니다.

    use std::sync::mpsc::channel;
    use std::thread;
    use std::collections::HashMap;
    use std::sync::Arc;
    
    fn main() {
        let (tx, rx) = channel();
        let mut m = HashMap::new();
        
        m.insert("a", "1");
        m.insert("b", "2");
     
        let arc = Arc::new(m); // Tells Rust we're sharing state
    
        for _ in 0..8 {
            let tx = tx.clone();
            let arc = arc.clone();
            thread::spawn(move || {
                let msg = format!("Accessed {:?}", arc);
                tx.send(msg).unwrap();
            });
        }
    
        drop(tx); // Effectively closes the channel
    
        for data in rx {
            println!("{:?}", data);
        }
    }

    RUST Playground

    그래도 뭐 코드가 막 눈에 익지는 않아요. 여기서 우린 GO 리더 빌리티가 얼마나 뛰어났는지 감탄하지 않을 수 없어요. 저기 보이는 unwrap(); send의 ok()와 panic를 분기해 줍니다. panic를 발생시키는 코드가 되겠죠. 

    insert는 thread spwan 되기 전에 main thread에서 처리되므로 Race-Condition은 아니고요. 

    Arc , Atomic reference count를 사용해서 메모리 더 이상 참조하지 않았을 경우 자동적으로 소멸하게 하는군요. 특히 스레드에서 뎅글러 포인터를 만날 이유가 없어졌군요.
    Clone(): tx.clone()은 여러 스레드에서 역시 단일 producer로 되어야 하고요. 또한 Arc Clone()을 사용하면 각 스레드에 기준 카운트와 일치하는 맵에 대한 읽기 전용 핸들이 생길 수 있겠어요.  

    Arc를 이용해서 메모리를 공유를 어떻게 처리하는지 확인해봤어요. 이제 위쪽의 GO의 로직을 생각하면서 RUST로 다시 작성해 보죠. 

    use std::sync::mpsc::channel;
    use std::collections::HashMap;
    use std::thread;
    use std::sync::{Arc, Mutex};
    
    fn main() {
        let (c_tx, c_rx) = channel();
        let m = HashMap::new();
    
        // Wrap m with a Mutex, wrap the mutex with Arc
        let arc = Arc::new(Mutex::new(m));
    
        let t_arc = arc.clone();
        thread::spawn(move || {
            let mut z = t_arc.lock().unwrap();
            z.insert("1", "a");
            c_tx.send(true).unwrap();
        });
    
        // Extra scope is needed to avoid the deadlock
        {
            let mut x = arc.lock().unwrap();
            x.insert("2", "b");
        }
    
        c_rx.recv().unwrap();
    
        for (k, v) in m.iter() {
            println!("{}, {}", k, v);
        }
    }
    

     

     

    Rust Playground

    write lock를 적절히 걸어주고 있습니다. 물로 arc는 lock() scope를 벗어나면 unlock이 되겠죠. 저도 이 부분은 참 편리한 부분이라 생각해요. 
    통상 GO 에선 defer unlock()이라고 코드 하죠. 

    결과적으로 위에 코드 borrow detector에 의해서 컴파일되지 않습니다. 다음에 이유 때문이죠. 

    Compiling playground v0.0.1 (/playground)
    error[E0382]: borrow of moved value: `m`
      --> src/main.rs:29:19
       |
    9  |     let m = HashMap::new();
       |         - move occurs because `m` has type `HashMap<&str, &str>`, which does not implement the `Copy` trait
    ...
    12 |     let arc = Arc::new(Mutex::new(m));
       |                                   - value moved here
    ...
    29 |     for (k, v) in m.iter() {
       |                   ^ value borrowed here after move
    
    error: aborting due to previous error
    
    For more information about this error, try `rustc --explain E0382`.
    error: could not compile `playground`
    
    To learn more, run the command again with --verbose.

    이제 m은 mutex 가드로 이동했죠 러스트의 ownership 모델 덕에 mutex는 이제 m에 ownership을 가지게 되고 관련 멤버 메서드의 
    lock() 들에 해달 될 때만 액세스가 가능하게 된 거죠. 
    뮤 텍스가 이 데이터를 모두 관리하게 된 겁니다.
    다시 보면 Object model에 contruct mode에서 특정 데이터를 injection 하고 관련 클래스가 핸들링하는 패턴을 떠올리시면 되겠네요.
    자 ownership 이 move 된 상황에서 borrow 한 겁니다. 이제 borrow detector는 이건 잘 못된 borrowing이라 표현합니다. 
    아 XX 귀찮아.. 이러겠죠.. 이해합니다. GO의 놀라운 생산성이 그리운 시기가 옵니다.. 

    하지만 borrow checker는 go가 체크할 수 없는 것을 추가적으로 체크 해줍니다. 다시 말해서 runtime시에 나는 거 보다는 났다는 거죠. 

    큰일이네요... 슬슬 맘에 들기 시작합니다... 넷츠케이프도 안되고 파폭도 안 되는 모질라 제단인데요... 구글이 저번에 보안사고도 내고 사실 돼지처럼 변하고 있죠.. 심지어 이 자식들은 배당금도 없어요.. 

    아래와 같이 수정을 좀 해볼게요. 

    use std::sync::mpsc::channel;
    use std::collections::HashMap;
    use std::thread;
    use std::sync::{Arc, Mutex};
    
    fn main() {
        let (c_tx, c_rx) = channel();
        let m = HashMap::new();
    
        // Wrap m with a Mutex, wrap the mutex with Arc
        let arc = Arc::new(Mutex::new(m));
    
        let t_arc = arc.clone();
        thread::spawn(move || {
            let mut z = t_arc.lock().unwrap();
            z.insert("1", "a");
            c_tx.send(true).unwrap();
        });
    
        // Extra scope is needed to avoid the deadlock
        {
            let mut x = arc.lock().unwrap();
            x.insert("2", "b");
        }
    
        c_rx.recv().unwrap();
    
        let a = arc.lock().unwrap();
        for (k, v) in a.iter() {
            println!("{}, {}", k, v);
        }
    }
    

     

    RUST Playground 

    {
    let mut x....  
    }

    Dead lock를 막기 위해서 scope를 정했습니다. 그리고 ownership를 x에 borrow 합니다. 

    c_rx.recv(). unwrap() 채널에 메세시를 받어요. 
    a 변수에 borrow 합니다.(m를 쓰지 않아요 ) 
    이상 없이 compile 되는 걸 확인할 수 있어요. 

     

    자 결과적으로

    GO / Rust 모두 mutext를 통해 무결성(integrity of shared state)을 보호할 수 있어요. 다른 점은 러스트는 코드를 이용하지 않고 직접 Lock를 건다는 겁니다. 
    > 데이터 레이스를 방지하는데 큰 도움을 줍니다. 
    > 공유 데이터를 관리하기 위해 sharing state를 써야 합니다. 물론 Rust의 친구는 clone()이라고 하더군요 ㅎㅎ 
    > Rust는 코드가 아닌 Ownership model를 이용해서 적절한 동기화가 되도록 보장합니다. 

     

    Borrowing and Ownership 이 중요해 보입니다. 


    국문으로 번역된 매뉴얼도
    Borrowing를 빌림 이라고 표현하고 있어요.. 빌림 채커 빌림..

     

    Result와 함께하는 복구 가능한 에러 - The Rust Programming Language

    대부분의 에러는 프로그램을 전부 멈추도록 요구될 정도로 심각하지는 않습니다. 종종 어떤 함수가 실패할 때는, 우리가 쉽게 해석하고 대응할 수 있는 이유에 대한 것입니다. 예를 들어, 만일

    rinthel.github.io

    그래서 저는 가급적  Youtube를 통해서 개념을 익히시기를 추천합니다. 

     

    개인적으로 GO가 쓰이는 곳이 더 많을 거 같아요.

    중요한건 어디에 어떻게 효율적으로 쓸 수 있는가 이겠죠?

    예를 들어 가능동에 와서 영어를 계속 쓴다고 고집하면 그건 어쩜 불편한 일일 수 있고요. 센 프란시코에 가서 지 멋이라고 제주도 말을 쓰면 그게 뭐 X수작의 한부분이 되겠죠. 

    물론 그래도 말은 통할 거예요 우린 다 같은 사람이니까요. 

     

    개인적으로 RUST / GO 두 가지를 다 해보기를 추천합니다. 적절한 프로젝트에 사용한다면 뭐 더 막강한 엔지니어가 될 수 있겠어요!!!!

    막강한 엔지니어가 되기전에 현재 여러분이 합리적으로 움직이고 있는지 조용히 아침에 일어나서 생각해 보셨으면 좋겠어요.

    이 혼돈의 시기에 내가 어디에 시간을 써야 되지? 우리는 어디로 가고 있는걸까? 우리가 빠져있는 이 재미난 것들이 나에게 올바른 길을 제시해 주고는 있는지 그리고 내가 그동안 엔지니어로서 무시했던 일중에 틀린 것은 없는지... 

     

    한 가지라도 있다면 그걸 먼저 배우시길 간절히 바랍니다. 누군가 그걸 통해 당신 위에서 굴림하고 있을지 모르는 일이니까요. 

     

    반응형

    댓글

Designed by Tistory.