0%

Comparison Rust and C++ Memory Model

我對Rust也是初學者的狀態,以下是從A 30 minute introduction to Rust看來的,既然Rust被定位成System programming language,難免要拿來跟C++比一比。看看Ruat友什麼特別之處。

The power of ownership

在C/C++,很簡單可以寫出這樣的程式碼。

1
2
3
4
5
6
7
8
9
10
int* dangling(void)
{
int i = 1234;
return &i;
}
int add_one(void)
{
int* num = dangling();
return *num + 1;
}

i是存在Stack上,隨時都有被覆蓋過去的危險,不過現在主流的Compiler都會產生Warning,告訴你這段Code可能會有問題。
Rust版

1
2
3
4
5
6
7
8
9
10
11
12
13
fn dangling() -> &int {
let i = 1234;
return &i;
}

fn add_one() -> int {
let num = dangling();
return *num + 1;
}

fn main() {
add_one();
}

這段Code根本編譯不過,編譯器會告訴你i的lifecycle只存活於dangling中,不能轉移給外部使用。
如果將i分配到Heap的話,大概會是這樣

1
2
3
4
5
6
7
8
9
fn dangling() -> Box<int> {
let i = box 1234i;
return i;
}

fn add_one() -> int {
let num = dangling();
return *num + 1;
}

i的控制權由dnagling轉移到add_one,等到沒有用的時候,就會被釋放掉。相當於C++11的unique_ptr>

1
2
3
4
5
6
7
8
9
unique_ptr<int> dangling(void)
{
return unique_ptr<int>(new int(1234));
}
int add_one(void)
{
unique_ptr<int> num = dangling();
return *num + 1;
}

最大的差異在於,Box不能為null。雖然用途被侷限了,不過可以減少很多錯誤。
C++有太多方式可以存取記憶體了,而Rust只有一種,所以要寫出正確的C++ Code會比Rust難。

Owning concurrency

在Rust中,物件的控制權也可以在Task中轉移。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let numbers = vec![1i, 2i, 3i];

let (tx, rx) = channel();
tx.send(numbers);

spawn(proc() {
let numbers = rx.recv();
println!("{}", *numbers.get(0));
})
// Try to print a number from the original task
// println!("{}", *numbers.get(0));
}

C++11可以作到同樣的事情

1
2
3
4
5
6
7
8
9
10
11
void func(vector<int> v)
{
cout << v[0] << endl;
}
int main() {
vector<int> numbers = { 1, 2, 3 };
thread t1(func, std::move(numbers));
t1.join();
//cout << numbers[0] << endl;
return 0;
}

最大的差異,將上面註解拿掉,可以看到Rust可以在Compile-time偵測出錯誤,而C++需要在Runtime才會知道。
如果要在多的Task中使用同一份資料時,可以採取以下兩種方法

  • 每個Task都拿到一份副本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn main() {
    let numbers = vec![1i, 2i, 3i];

    for num in range(0u, 3) {
    let (tx, rx) = channel();
    // Use `clone` to send a *copy* of the array
    tx.send(numbers.clone());

    spawn(proc() {
    let numbers = rx.recv();
    println!("{:d}", *numbers.get(num as uint));
    })
    }
    }
    當Task很多或是資料很大時,這個方案就不適用了。
  • Atomically reference counted (ARC)
    Rust推薦的方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    use std::sync::Arc;

    fn main() {
    let numbers = vec![1i, 2i, 3i];
    let numbers = Arc::new(numbers);

    for num in range(0u, 3) {
    let (tx, rx) = channel();
    tx.send(numbers.clone());

    spawn(proc() {
    let numbers = rx.recv();
    // *numbers.get_mut(num as uint) = *numbers.get_mut(num as uint) + 1;
    println!("{:d}", *numbers.get(num as uint));
    })
    }
    }
    相當於C++11 shared_ptr的應用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <iostream>
    #include <thread>
    #include <vector>
    #include <memory>
    using namespace std;
    void func(shared_ptr<vector<int>> &v, int i)
    {
    cout << (*v)[i] << endl;
    }
    int main() {
    shared_ptr<vector<int>> number(new vector < int > { 1, 2, 3 });
    thread t[3];
    for (int i = 0; i < 3; i++)
    {
    thread t1 = thread(func, std::ref(number), i);
    t[i] = std::move(t1);
    }
    for (int i = 0; i < 3; i++)
    t[i].join();
    return 0;
    }
    最大的差異在於,Rust的Arc是immutable※,而C++是mutable
    就算寫成這樣,也無法改變資料被改變的事實。除此之外,也有可能出現Race condtion。immutable代表著不需要加Lock也能放新的在Thread中共享數據。
    1
    2
    3
    4
    5
    void func(const shared_ptr<vector<int>> &v, int i)
    {
    cout << (*v)[i] << endl;
    (*v)[i] = (*v)[i] + 1;
    }
    這邊的const,只能保護v這個值不被更改,對於v所指的內容無法保護。
    如果要在多個Task中修改的話,就只能這麼做
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    use std::sync::{Arc, Mutex};

    fn main() {
    let numbers = vec![1i, 2i, 3i];
    let numbers_lock = Arc::new(Mutex::new(numbers));

    for num in range(0u, 3) {
    let (tx, rx) = channel();
    tx.send(numbers_lock.clone());

    spawn(proc() {
    let numbers_lock = rx.recv();

    // Take the lock, along with exclusive access to the underlying array
    let mut numbers = numbers_lock.lock();

    // This is ugly for now, but will be replaced by
    // `numbers[num as uint] += 1` in the near future.
    // See: https://github.com/rust-lang/rust/issues/6515
    *numbers.get_mut(num as uint) = *numbers.get_mut(num as uint) + 1;

    println!("{}", *numbers.get(num as uint));

    // When `numbers` goes out of scope the lock is dropped
    })
    }
    }
    這邊就等同於上面的例子加上個Mutex保護,這邊就不多寫了。
    跟現在的C++11比較起來,Owenership這邊可以靠unique_ptr來處理,不過根本性的差異在於Rust認為資料預設為immutable的,所以很多問題可以簡化,透過編譯氣在Compile-time可以偵測出很多問題,而C++只能靠Programmer花時間檢測。

Reference