-
Go언어 시작하기 - Go developer를 위한 Rust 패러다임 / Paradigms of RUST for GO developersMedia_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); } }
좀 달라 보이긴 하는데 일단 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); } }
그래도 뭐 코드가 막 눈에 익지는 않아요. 여기서 우린 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); } }
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); } }
{
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를 빌림 이라고 표현하고 있어요.. 빌림 채커 빌림..그래서 저는 가급적 Youtube를 통해서 개념을 익히시기를 추천합니다.
개인적으로 GO가 쓰이는 곳이 더 많을 거 같아요.
중요한건 어디에 어떻게 효율적으로 쓸 수 있는가 이겠죠?
예를 들어 가능동에 와서 영어를 계속 쓴다고 고집하면 그건 어쩜 불편한 일일 수 있고요. 센 프란시코에 가서 지 멋이라고 제주도 말을 쓰면그게 뭐 X수작의 한부분이 되겠죠.물론 그래도 말은 통할 거예요 우린 다 같은 사람이니까요.
개인적으로 RUST / GO 두 가지를 다 해보기를 추천합니다. 적절한 프로젝트에 사용한다면 뭐 더 막강한 엔지니어가 될 수 있겠어요!!!!
막강한 엔지니어가 되기전에 현재 여러분이 합리적으로 움직이고 있는지 조용히 아침에 일어나서 생각해 보셨으면 좋겠어요.
이 혼돈의 시기에 내가 어디에 시간을 써야 되지? 우리는 어디로 가고 있는걸까? 우리가 빠져있는 이 재미난 것들이 나에게 올바른 길을 제시해 주고는 있는지 그리고 내가 그동안 엔지니어로서 무시했던 일중에 틀린 것은 없는지...
한 가지라도 있다면 그걸 먼저 배우시길 간절히 바랍니다. 누군가 그걸 통해 당신 위에서 굴림하고 있을지 모르는 일이니까요.
반응형'Media_Dev' 카테고리의 다른 글
Atoms, Boxes, Parents, Children & hex (0) 2021.01.07 Atom(Box) Viewer (0) 2020.12.21 Spoon Job Description [스푼라디오] 미디어 서버 개발자 (Media Server Developer) 추천 (0) 2020.11.18 Who are the WebRTC Market Global Key Players? (0) 2020.03.05 CMAF Chunked for low latency (0) 2020.02.17