Daily Archives: 25/01/2018

Rust – Concurrency và thread

Concurrency là khả năng chạy nhiều tác vụ song song tại một thời điểm và những tác vụ này có thể tương tác với nhau.

Một ứng dụng Rust khi chạy thì sẽ là tập hợp của rất nhiều luồng (thread) của hệ điều hành mà ứng dụng đang chạy. Bản thân mỗi ứng dụng Rust cũng có thể có các luồng của riêng nó.

Trong các bài trước thì chúng ta chủ yếu làm việc với một luồng chính là main(). Trong bài này chúng ta sẽ tìm hiểu cách tạo ra các luồng khác và các luồng có thể chia sẻ dữ liệu với nhau.

Tạo luồng

Để có thể tạo luồng thì chúng ta phải dùng tới module std::thread, và để tạo luồng thì chúng ta sẽ gọi hàm spawn(). Ví dụ:

use std::thread;
fn main() {
thread::spawn(move || {
println!("Hello World printed from another thread");
});
}

Tham số của hàm spawn() là một hàm closure, nếu bạn chưa biết hàm closure là gì thì có thể giải thích đơn giản là đây là các hàm được định nghĩa bên trong một hàm khác. Trong trường hợp này là macro println!() được gọi bên trong cặp dấu {}, tham số sẽ được truyền trong cặp dấu ||, ở đây chúng ta không truyền vào tham số nào (nên 2 dấu | đứng một mình).

Khi chạy thì chúng ta sẽ không thấy đoạn text trong println!() được in ra, lí do là vì luồng con được tạo ra có thể vẫn còn chạy trong khi luồng cha là main() đã kết thúc trước đó. Chúng ta có thể cho luồng main() tạm dừng một khoảng thời gian ngắn để thấy output của luồng con như sau:

use std::thread;
fn main() {
thread::spawn(move || {
println!("Hello World printed from another thread");
});
thread::sleep_ms(50);
}

Hàm sleep_ms() sẽ làm cho luồng main() tạm dừng 50 mili giây và chúng ta có thể thấy được đoạn text in ra của luồng con.

Hello World printed from another thread

Một cách khác là chúng ta có thể dùng gán giá trị trả về của hàm spawn() cho một biến, sau đó dùng hàm join() để kết nối biến đó với luồng của nó, rồi dùng hàm unwrap() để lấy kết quả. Ví dụ:

use std::thread;

fn main() {
let wait = thread::spawn(move || {
println!("Hello World printed from another thread");
}); 
let result = wait.join().unwrap();
println!("{:?}", result);
}

Khi chúng ta gọi hàm join() để liên kết thì luồng main() sẽ bị buộc phải đợi cho luồng con chạy xong rồi mới chạy tiếp, sau đó hàm này sẽ trả về một đối tượng Result. Hàm unwarp() sẽ chuyển đối tượng này thành một đối tượng Ok hoặc Err tùy trường hợp có lỗi hay không.

 Hello World printed from another thread 

Bạn cũng có thể gọi thread::spawn(...).join(), làm thế này thì luồng chương trình sẽ là tuyến tính, tức là main() sẽ tạo luồng con, rồi đợi luồng con kết thúc mới tiếp tục.

Tạo nhiều luồng

Luồng trong Rust rất nhẹ, chúng ta có thể tạo ra khoảng 10.000 luồng chỉ trong vòng vài giây. Ví dụ:

use std::thread;

fn main() {
for i in 0..10000 {
thread::spawn(move || {
println!("Creating thread {}", i);
});
}
}

Bạn có thể thấy output được in ra rất lộn xộn, không có thứ tự vì các luồng có tốc độ chạy khác nhau.

Creating thread 4
Creating thread 23
Creating thread 48
Creating thread 13
Creating thread 293
...

Chúng ta cũng có thể tạo nhiều luồng bằng một crate có tên là threadpool. Đầu tiên chúng ta khai báo crate này trong Cargo.toml như sau:

[dependencies]
threadpool = "*"

Chúng ta sử dụng crate này trong main.rs như sau:

extern crate threadpool;

use std::thread;
use threadpool::ThreadPool;

fn main() {
let pool = ThreadPool::new(10);
for i in 0..10 {
pool.execute(move || {
println!("Creating thread {} using pool", i);});
}
thread::sleep_ms(50);
}

Chúng ta khai báo module ThreadPool trong crate threadpool, rồi gọi hàm new(10) để khai báo 10 luồng. Sau đó thay vì gọi spawn() như trong module thread thì chúng ta gọi hàm execute() để tạo luồng.

Creating thread 0 using pool
Creating thread 2 using pool
Creating thread 5 using pool
Creating thread 3 using pool
Creating thread 4 using pool
Creating thread 1 using pool
Creating thread 8 using pool
Creating thread 7 using pool
Creating thread 8 using pool
Creating thread 6 using pool

Panic trong các luồng

Như đã nói ở trên, các luồng độc lập với nhau, không liên quan gì với nhau cả, do đó khi một luồng panic thì các luồng khác vẫn chạy bình thường như không có gì xảy ra. Tuy nhiên luồng cha có thể kiểm tra xem một luồng con của nó có panic hay không bằng cách sử dụng hàm is_err(), ví dụ:

use std::thread;

fn main() {
let result = thread::spawn(move|| {
panic!("This thread is panicked");
}).join();

if result.is_err() {
println!("The child thread is panicked");
}
}

Đoạn code trên sẽ cho ra output như sau:

 
thread '<unnamed>' panicked at 'This thread is panicked', src\main.rs:5:8 
note: Run with `RUST_BACKTRACE=1` for a backtrace. 
The child thread is panicked

Bảo mật luồng

Khi làm việc với luồng trong các ngôn ngữ như C++ thì việc điều khiển luồng là rất khó khi các luồng có tương tác với nhau. Khi có 2, 3 hay nhiều luồng cùng chỉnh sửa dữ liệu thì dữ liệu rất dễ bị phá hỏng. Các ngôn ngữ khác thường không cho phép làm việc với luồng để tránh điều này.

Trong Rust thì các luồng sẽ làm việc với cơ chế ownership mà chúng ta đã tìm hiểu. Ví dụ:

use std::thread;

fn main() {
let mut x = 12;
for i in 2..5 {
thread::spawn(move || {
x *= i;
});
}
thread::sleep_ms(50);
println!("x = {}", x);
}

Trong đoạn code trên chúng ta tạo ra 3 thread cùng có thể chỉnh sửa giá trị của biến x, nhưng khi in giá trị của x trong hàm main() thì chúng ta vẫn sẽ được giá trị cũ của x, tức là x không bị thay đổi bởi các luồng, lý do là vì các luồng chỉ làm việc với một bản copy của x chứ không thay đổi trực tiếp lên x.

 x = 12