我們知道,如今cpu的計算能力已經非常強大,其速度比記憶體要高出許多個數量級。為了充分利用cpu資源,多數程式語言都提供了併發程式設計的能力,rust也不例外。
聊到併發,就離不開多程序和多執行緒這兩個概念。其中,程序是資源分配的最小單位,而執行緒是程式執行的最小單位。執行緒必須依託於程序,多個執行緒之間是共享程序的記憶體空間的。程序間的切換複雜,cpu利用率低等缺點讓我們在做併發程式設計時更加傾向於使用多執行緒的方式。
當然,多執行緒也有缺點。其一是程式執行順序不能確定,因為這是由核心來控制的,其二就是多執行緒程式設計對開發者要求比較高,如果不充分了解多執行緒機制的話,寫出的程式就非常容易出bug。
多執行緒程式設計的主要難點在於如何保證執行緒安全。什麼是執行緒安全呢?因為多個執行緒之間是共享記憶體空間的,因此就會存在同時對相同的記憶體進行寫操作,那就會出現寫入資料互相覆蓋的問題。如果多個執行緒對記憶體只有讀操作,沒有任何寫操作,那麼也就不會存在安全問題,我們可以稱之為執行緒安全。
常見的併發安全問題有競態條件和資料競爭兩種,競態條件是指多個執行緒對相同的記憶體區域(我們稱之為臨界區)進行了「讀取-修改-寫入」這樣的操作。而資料競爭則是指乙個執行緒寫乙個變數,而另乙個執行緒需要讀這個變數,此時兩者就是資料競爭的關係。這麼說可能不太容易理解,不過不要緊,待會兒我會舉兩個具體的例子幫助大家理解。不過在此之前,我想先介紹一下rust中是如何進行併發程式設計的。
在rust標準庫中,提供了兩個包來進行多執行緒程式設計:
我們使用std::thread中的spawn
函式來建立執行緒,它的使用非常簡單,其引數是乙個閉包,傳入建立的執行緒需要執行的程式。
use std::thread;
use std::time::duration;
fn main() from the spawned thread!", i);
thread::sleep(duration::from_millis(1));
}});
for i in 1..5 from the main thread!", i);
thread::sleep(duration::from_millis(1));}}
這段**中,我們有兩個執行緒,乙個主線程,乙個是用spawn
建立出來的執行緒,兩個執行緒都執行了乙個迴圈。迴圈中列印了一句話,然後讓執行緒休眠1毫秒。它的執行結果是這樣的:
從結果中我們能看出兩件事:第一,兩個執行緒是交替執行的,但是並沒有嚴格的順序,第二,當主線程結束時,它並沒有等子執行緒執行完。
那我們有沒有辦法讓主線程等子執行緒執行結束呢?答案當然是有的。rust中提供了join
函式來解決這個問題。
use std::thread;
use std::time::duration;
fn main() from the spawned thread!", i);
thread::sleep(duration::from_millis(1));
}});
for i in 1..5 from the main thread!", i);
thread::sleep(duration::from_millis(1));
}handle.join().unwrap();
}
這樣主線程就必須要等待子執行緒執行完畢。
use std::thread;
fn main() ", v);
});handle.join().unwrap();
}
使用thread::spawn
建立執行緒是不是非常簡單。但是也是因為它的簡單,所以可能無法滿足我們一些定製化的需求。例如制定執行緒的棧大小,執行緒名稱等。這時我們可以使用thread::builder
來建立執行緒。
use std::thread::;
fn main() ", id);
let size: usize = 3 * 1024;
let builder = builder::new().name(thread_name).stack_size(size);
let child = builder.spawn(move || ", current().name().unwrap());
}).unwrap();
v.push(child);
}for child in v
}
我們使用thread::spawn
建立的執行緒返回的型別是joinhandle
,而使用builder.spawn
返回的是result>
,因此這裡需要加上unwrap
方法。
除了剛才提到了這些函式和結構體,std::thread
還提供了一些底層同步原語,包括park、unpark和yield_now函式。其中park提供了阻塞執行緒的能力,unpark用來恢復被阻塞的執行緒。yield_now函式則可以讓執行緒放棄時間片,讓給其他執行緒執行。
聊完了執行緒管理,我們再回到執行緒安全的話題,rust提供的這些執行緒管理工具看起來和其他沒有什麼區別,那rust又是如何保證執行緒安全的呢?
秘密就在send
和sync
這兩個trait中。它們的作用是:
現在我們可以看一下spawn
函式的原始碼
#[stable(feature = "rust1", since = "1.0.0")]
pub fn spawn(f: f) -> joinhandlewhere
f: fnonce() -> t, f: send + 'static, t: send + 'static
在rust入坑指南:智慧型指標一文中,我們介紹了共享所有權的指標rc
,但在多執行緒之間共享變數時,就不能使用rc
,因為它的內部不是原子操作。不過不要緊,rust為我們提供了執行緒安全版本:arc
。
下面我們一起來驗證一下。
use std::thread;
use std::rc::rc;
fn main() );}}
這個程式會報如下錯誤
那我們把rc
替換為arc
試一下。
use std::sync::arc;
...let mut s = arc::new("hello".to_string());
很遺憾,程式還是報錯。
這是因為,arc預設是不可變的,我們還需要提供內部可變性。這時你可能想到來refcell,但是它也是執行緒不安全的。所以這裡我們需要使用mutex
型別。它是rust實現的互斥鎖。
rust中使用mutex
實現互斥鎖,從而保證執行緒安全。如果型別t實現了send,那麼mutex
會自動實現send和sync。它的使用方法也比較簡單,在使用之前需要通過lock
或try_lock
方法來獲取鎖,然後再進行操作。那麼現在我們就可以對前面的**進行修復了。
use std::thread;
use std::sync::;
fn main() );
v.push(child);
}for child in v
}
介紹完了互斥鎖之後,我們再來了解一下rust中提供的另外一種鎖——讀寫鎖rwlock
。互斥鎖用來獨佔執行緒,而讀寫鎖則可以支援多個讀執行緒和乙個寫執行緒。
在使用讀寫鎖時要注意,讀鎖和寫鎖是不能同時存在的,在使用時必須要使用顯式作用域把讀鎖和寫鎖隔離開。
本文我們先是介紹了rust管理執行緒的兩個函式:spawn
、join
。並且知道了可以使用builder結構體定製化建立執行緒。然後又學習了rust提供執行緒安全的兩個trait,send和sync。最後我們一起學習了rust提供的兩種鎖的實現:互斥鎖和讀寫鎖。
關於rust併發程式設計坑還沒有到底,接下來還有條件變數、原子型別這些坑等著我們來挖。今天就暫時歇業了。
rust實現wss訪問 Rust入坑指南 居安思危
任何事情都是相對的,就像rust給我們的印象一直是安全 快速,但實際上,完全的安全是不可能實現的。因此,rust中也是會有不安全的 的。嚴格來講,rust語言可以分為safe rust和unsafe rust。unsafe rust是safe rust的超集。在unsafe rust中並不會禁用任何...
rust怎麼關陽光指令 Rust入坑指南 步步為營
俗話說 測試寫得好,獎金少不了。寫單元測試一般需要三個步驟 準備測試用例,測試用例要能覆蓋盡可能多的 執行需要測試的 判斷結果,是否是你希望得到的結果 了解了這些以後,我們就來看看在rust中應該怎麼寫單元測試。首先我們建立乙個library專案 cargo new adder lib create...
Rust入坑指南 步步為營
俗話說 測試寫得好,獎金少不了。有經驗的開發人員通常會通過單元測試來保證 基本邏輯的正確性。如果你是一名新手開發者,並且還沒體會到單元測試的好處,那麼建議你先讀一下我之前的一篇文章 潔癖系列 七 單元測試的地位。寫單元測試一般需要三個步驟 準備測試用例,測試用例要能覆蓋盡可能多的 執行需要測試的 判...