# 并發
> [concurrency.md](https://github.com/rust-lang/rust/blob/master/src/doc/book/concurrency.md)
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4
并發與并行是計算機科學中相當重要的兩個主題,并且在當今生產環境中也十分熱門。計算機正擁有越來越多的核心,然而很多程序員還沒有準備好去完全的利用它們。
Rust 的內存安全功能也適用于并發環境。甚至并發的 Rust 程序也會是內存安全的,并且沒有數據競爭。Rust 的類型系統也能勝任,并且在編譯時能提供你強大的方式去推論并發代碼。
在我們討論 Rust 提供的并發功能之前,理解一些問題是很重要的:Rust 非常底層以至于所有這些都是由標準庫,而不是由語言提供的。這意味著如果你在某些方面不喜歡 Rust 處理并發的方式,你可以自己實現一個。[mio](https://github.com/carllerche/mio)是關于這個原則實踐的一個實際的例子。
### 背景:`Send`和`Sync`
并發難以推理。在 Rust 中,我們有一個強大、靜態類型系統來幫助我們推理我們的代碼。Rust 自身提供了兩個特性來幫助我們理解可能是并發的代碼的意思。
### `Send`
第一個我們要談到的特性是[Send](http://doc.rust-lang.org/std/marker/trait.Send.html)。當一個`T`類型實現了`Send`,它向編譯器指示這個類型的所有權可以在線程間安全的轉移。
強制實施一些通用的限制是很重要的。例如,我們有一個連接兩個線程的通道,我們想要能夠向通道發送些數據到另一個線程。因此,我們要確保這個類型實現了`Send`。
相反的,如果我們通過 FFI 封裝了一個不是線程安全的庫,我們并不想實現`Send`,那么編譯器會幫助我們強制確保它不會離開當前線程。
### `Sync`
第二個特性是`Sync`。當一個類型`T`實現了`Sync`,它向編譯器指示這個類型在多線程并發時沒有導致內存不安全的可能性。這隱含了沒有[內部可變性](#)的類型天生是`Sync`的,這包含了基本類型(如 `u8`)和包含他們的聚合類型。
為了在線程間共享引用,Rust 提供了一個叫做`Arc<T>`的 wrapper 類型。`Arc<T>`實現了`Send`和`Sync`當且僅當`T`實現了`Send`和`Sync`。例如,一個`Arc<RefCell<U>>`類型的對象不能在線程間傳送因為[RefCell](#)并沒有實現`Sync`,因此`Arc<RefCell<U>>`并不會實現`Send`。
這兩個特性允許你使用類型系統來確保你代碼在并發環境的特性。在我們演示為什么之前,我們需要先學會如何創建一個并發 Rust 程序!
### 線程
Rust標準庫提供了一個“線程”庫,它允許你并行的執行 Rust 代碼。這是一個使用`std::thread`的基本例子:
~~~
use std::thread;
fn main() {
thread::spawn(|| {
println!("Hello from a thread!");
});
}
~~~
`thread::spawn()`方法接受一個閉包,它將會在一個新線程中執行。它返回一線程的句柄,這個句柄可以用來等待子線程結束并提取它的結果:
~~~
use std::thread;
fn main() {
let handle = thread::spawn(|| {
"Hello from a thread!"
});
println!("{}", handle.join().unwrap());
}
~~~
很多語言有執行多線程的能力,不過是很不安全的。有完整的書籍是關于如何避免在共享可變狀態下出現錯誤的。在此,借助類型系統,Rust也通過在編譯時避免數據競爭來幫助我們。讓我們具體討論下如何在線程間共享數據。
### 安全的共享可變狀態(Safe Shared Mutable State)
根據Rust的類型系統,我們有個聽起來類似謊言的概念叫做:“安全的共享可變狀態”。很多程序員都同意共享可變狀態是非常,非常不好的。
有人曾說道:
> 共享可變狀態是一切罪惡的根源。大部分語言嘗試解決這個問題的“可變”部分,而Rust則嘗試解決“共享”部分。
同樣[所有權系統](#)也通過防止不當的使用指針來幫助我們排除數據競爭,最糟糕的并發bug之一。
作為一個例子,這是一個在很多語言中會產生數據競爭的 Rust 版本程序。它不能編譯:
~~~
use std::thread;
use std::time::Duration;
fn main() {
let mut data = vec![1, 2, 3];
for i in 0..3 {
thread::spawn(move || {
data[i] += 1;
});
}
thread::sleep(Duration::from_millis(50));
}
~~~
這會給我們一個錯誤:
~~~
8:17 error: capture of moved value: `data`
data[i] += 1;
^~~~
~~~
Rust 知道這并不是安全的!如果每個線程中都有一個`data`的引用,并且這些線程獲取了引用的所有權,我們就有了3個所有者!
所以,我們需要能讓一個值有多于一個引用并可用于線程間共享的某種類型,就是說它必須實現 `Sync`。
我們將使用`Arc<T>`,Rust 的標準原子引用計數類型,它用一些額外的運行時記錄包裝了一個值,它允許我們同時在多個引用間共享值的所有權。
這些記錄包含了值一共有多少個這樣的引用,也就是它的名字中的引用計數部分。
`Arc<T>`的原子部分可以在多線程中安全的訪問。為此編譯器確保了內部計數的改變都是不可分割的操作這樣就不會產生數據競爭。
~~~
use std::thread;
use std::sync::Arc;
use std::time::Duration;
fn main() {
let mut data = Arc::new(vec![1, 2, 3]);
for i in 0..3 {
let data = data.clone();
thread::spawn(move || {
data[i] += 1;
});
}
thread::sleep(Duration::from_millis(50));
}
~~~
現在我們在`Arc<T>`上調用`clone()`,它增加了內部計數。接著這個句柄被移動到了新線程。
同時。。。仍然出錯了。
~~~
:11:24 error: cannot borrow immutable borrowed content as mutable
:11 data[i] += 1;
^~~~
~~~
`Arc<T>`假設它的內容有另一個屬性來確保它可以安全的在線程間共享:它假設它的內容是`Sync`的。這在我們的值是不可變時為真,不過我們想要能夠改變它,所以我們需要一些別的方法來說服借用檢查器我們知道我們在干什么。
看起來我們需要一些允許我們安全的改變共享值的類型,例如同一時刻只允許一個線程能夠它內部值的類型。
為此,我們可以使用`Mutex<T>`類型!
下面是一個可以工作的版本:
~~~
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
for i in 0..3 {
let data = data.clone();
thread::spawn(move || {
let mut data = data.lock().unwrap();
data[i] += 1;
});
}
thread::sleep(Duration::from_millis(50));
}
~~~
注意`i`的值被限制(拷貝)到了閉包里并不是在線程間共享。
同時注意到[Mutex](http://doc.rust-lang.org/std/sync/struct.Mutex.html)的[lock](http://doc.rust-lang.org/std/sync/struct.Mutex.html#method.lock)方法有如下簽名:
~~~
fn lock(&self) -> LockResult<MutexGuard<T>>
~~~
并且因為`MutexGuard<T>`并沒有實現`Send`,guard 并不能跨線程,確保了線程局部性鎖的獲取和釋放。
讓我們更仔細的檢查一個線程代碼:
~~~
# use std::sync::{Arc, Mutex};
# use std::thread;
# use std::time::Duration;
# fn main() {
# let data = Arc::new(Mutex::new(vec![1, 2, 3]));
# for i in 0..3 {
# let data = data.clone();
thread::spawn(move || {
let mut data = data.lock().unwrap();
data[i] += 1;
});
# }
# thread::sleep(Duration::from_millis(50));
# }
~~~
首先,我們調用`lock()`,它獲取了互斥鎖。因為這可能失敗,它返回一個`Result<T, E>`,并且因為這僅僅是一個例子,我們`unwrap()`結果來獲得一個數據的引用。現實中的代碼在這里應該有更健壯的錯誤處理。下面我們可以隨意修改它,因為我們持有鎖。
最后,在線程運行的同時,我們等待在一個較短的定時器上。不過這并不理想:我們可能選擇等待了一個合理的時間不過它更可能比所需的時間要久或并不足夠長,這依賴程序運行時線程完成它的計算所需的時間。
一個比定時器更精確的替代是使用一個 Rust 標準庫提供的用來同步各個線程的機制。讓我們聊聊其中一個:通道。
### 通道(Channels)
下面是我們代碼使用通道同步的版本,而不是等待特定時間:
~~~
use std::sync::{Arc, Mutex};
use std::thread;
use std::sync::mpsc;
fn main() {
let data = Arc::new(Mutex::new(0));
let (tx, rx) = mpsc::channel();
for _ in 0..10 {
let (data, tx) = (data.clone(), tx.clone());
thread::spawn(move || {
let mut data = data.lock().unwrap();
*data += 1;
tx.send(()).unwrap();
});
}
for _ in 0..10 {
rx.recv().unwrap();
}
}
~~~
我們使用`mpsc::channel()`方法創建了一個新的通道。我們僅僅向通道中`send`了一個簡單的`()`,然后等待它們10個都返回。
因為這個通道只是發送了一個通用信號,我們也可以通過通道發送任何實現了`Send`的數據!
~~~
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
for i in 0..10 {
let tx = tx.clone();
thread::spawn(move || {
let answer = i * i;
tx.send(answer).unwrap();
});
}
for _ in 0..10 {
println!("{}", rx.recv().unwrap());
}
}
~~~
這里我們創建了 10 個線程,分別計算一個數字的平方(`spawn()`時的`i`),接著通過通道把結果`send()`回主線程。
### 恐慌(Panics)
`panic!`會使當前執行線程崩潰。你可以使用 Rust 的線程來作為一個簡單的隔離機制:
~~~
use std::thread;
let handle = thread::spawn(move || {
panic!("oops!");
});
let result = handle.join();
assert!(result.is_err());
~~~
我們的`Thread`返回一個`Result`,它允許我們檢查我們的線程是否發生了恐慌。
- 前言
- 貢獻者
- 1.介紹
- 2.準備
- 3.學習 Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的 Rust
- 4.語法和語義
- 4.1.變量綁定
- 4.2.函數
- 4.3.原生類型
- 4.4.注釋
- 4.5.If語句
- 4.6.循環
- 4.7.所有權
- 4.8.引用和借用
- 4.9.生命周期
- 4.10.可變性
- 4.11.結構體
- 4.12.枚舉
- 4.13.匹配
- 4.14.模式
- 4.15.方法語法
- 4.16.Vectors
- 4.17.字符串
- 4.18.泛型
- 4.19.Traits
- 4.20.Drop
- 4.21.if let
- 4.22.trait 對象
- 4.23.閉包
- 4.24.通用函數調用語法
- 4.25.crate 和模塊
- 4.26.const和static
- 4.27.屬性
- 4.28.type別名
- 4.29.類型轉換
- 4.30.關聯類型
- 4.31.不定長類型
- 4.32.運算符和重載
- 4.33.Deref強制多態
- 4.34.宏
- 4.35.裸指針
- 4.36.不安全代碼
- 5.高效 Rust
- 5.1.棧和堆
- 5.2.測試
- 5.3.條件編譯
- 5.4.文檔
- 5.5.迭代器
- 5.6.并發
- 5.7.錯誤處理
- 5.8.選擇你的保證
- 5.9.外部函數接口
- 5.10.Borrow 和 AsRef
- 5.11.發布途徑
- 5.12.不使用標準庫
- 6.Rust 開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接進階
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 6.11.自定義內存分配器
- 7.詞匯表
- 8.語法索引
- 9.參考文獻
- 附錄:名詞中英文對照