如何實現一個執行緒池
執行緒池:一種執行緒使用模式。執行緒過多會帶來排程開銷,進而影響快取區域性性和整體效能。而執行緒池維護著多個執行緒,等待著監督管理者分配可併發執行的任務。這避免了在處理短時間任務時建立與銷燬執行緒的代價。執行緒池不僅能夠保證核心的充分利用,還能防止過分排程。可用執行緒數量應該取決於可用的併發處理器、處理器核心、記憶體、網路sockets等的數量。 例如,對於計算密集型任務,執行緒數一般取cpu數量+2比較合適,執行緒數過多會導致額外的執行緒切換開銷。
如何定義執行緒池Pool呢,首先最大執行緒數量肯定要作為執行緒池的一個屬性,並且在new Pool時建立指定的執行緒。
執行緒池Pool
pub struct Pool {
max_workers: usize, // 定義最大執行緒數
}
impl Pool {
fn new(max_workers: usize) -> Pool {}
fn execute<F>(&self, f:F) where F: FnOnce() + 'static + Send {}
}
用execute
來執行任務,F: FnOnce() + 'static + Send
是使用thread::spawn執行緒執行需要滿足的trait, 代表F是一個能線上程裡執行的閉包函式。
另一點自然而然會想到在Pool新增一個執行緒陣列, 這個執行緒陣列就是用來執行任務的。比如Vec<Thread>
balabala。這裡的執行緒是活的,是一個個不斷接受任務然後執行的實體。
可以看作在一個執行緒裡不斷執行獲取任務並執行的Worker。
struct Worker where
{
_id: usize, // worker 編號
}
要怎麼把任務傳送給Worker執行呢?mpsc(multi producer single consumer) 多生產者單消費者可以滿足我們的需求,let (tx, rx) = mpsc::channel()
可以獲取到一對傳送端和接收端。
把傳送端新增到Pool裡面,把接收端新增到Worker裡面。Pool通過channel將任務傳送給多個worker消費執行。
這裡有一點需要特別注意,channel的接收端receiver需要安全的在多個執行緒間共享,因此需要用Arc<Mutex::<T>>
來包裹起來,也就是用鎖來解決併發衝突。
Pool的完整定義
pub struct Pool {
workers: Vec<Worker>,
max_workers: usize,
sender: mpsc::Sender<Message>
}
該是時候定義我們要發給Worker的訊息Message了
定義如下的列舉值
type Job = Box<dyn FnOnce() + 'static + Send>;
enum Message {
ByeBye,
NewJob(Job),
}
Job是一個要傳送給Worker執行的閉包函式,這裡ByeBye用來通知Worker可以終止當前的執行,退出執行緒。
只剩下實現Worker和Pool的具體邏輯了。
Worker的實現
impl Worker
{
fn new(id: usize, receiver: Arc::<Mutex<mpsc::Receiver<Message>>>) -> Worker {
let t = thread::spawn( move || {
loop {
let receiver = receiver.lock().unwrap();
let message= receiver.recv().unwrap();
match message {
Message::NewJob(job) => {
println!("do job from worker[{}]", id);
job();
},
Message::ByeBye => {
println!("ByeBye from worker[{}]", id);
break
},
}
}
});
Worker {
_id: id,
t: Some(t),
}
}
}
let message = receiver.lock().unwrap().recv().unwrap(); 這裡獲取鎖後從receiver獲取到訊息體,然後let message結束後rust的生命週期會自動釋放掉鎖。
但如果寫成
while let message = receiver.lock().unwrap().recv().unwrap() {
};
while let 後面整個括號都是一個作用域,要在這個作用域結束後,鎖才會釋放,比上面let message要鎖定久時間。
rust的mutex鎖沒有對應的unlock方法,由mutex的生命週期管理。
我們給Pool實現Drop
trait, 讓Pool被銷燬時,自動暫停掉worker執行緒的執行。
impl Drop for Pool {
fn drop(&mut self) {
for _ in 0..self.max_workers {
self.sender.send(Message::ByeBye).unwrap();
}
for w in self.workers.iter_mut() {
if let Some(t) = w.t.take() {
t.join().unwrap();
}
}
}
}
drop方法裡面用了兩個迴圈,而不是在一個迴圈裡做完兩件事?
for w in self.workers.iter_mut() {
if let Some(t) = w.t.take() {
self.sender.send(Message::ByeBye).unwrap();
t.join().unwrap();
}
}
這裡面隱藏了一個會造成死鎖的陷阱,比如兩個Worker, 在單個迴圈裡面迭代所有Worker,再將終止資訊傳送給通道後,直接呼叫join,
我們預期是第一個worker要收到訊息,並且等他執行完。當情況可能是第二個worker獲取到了訊息,第一個worker沒有獲取到,那接下來的join就會阻塞造成死鎖。
注意到沒有,Worker是被包裝在Option內的,這裡有兩個點需要注意
- t.join 需要持有t的所有權
- 在我們這種情況下,self.workers只能作為引用被for迴圈迭代。
這裡考慮讓Worker持有Option<JoinHandle<()>>
,後續可以通過在Option上呼叫take方法將Some變體的值移出來,並在原來的位置留下None變體。
換而言之,讓執行中的worker持有Some的變體,清理worker時,可以使用None替換掉Some,從而讓Worker失去可以執行的執行緒
struct Worker where
{
_id: usize,
t: Option<JoinHandle<()>>,
}
要點總結
- Mutex依賴於生命週期管理鎖的釋放,使用的時候需要注意是否逾期持有鎖
Vec<Option<T>>
可以解決某些情況下需要T所有權的場景
完整程式碼
use std::thread::{self, JoinHandle};
use std::sync::{Arc, mpsc, Mutex};
type Job = Box<dyn FnOnce() + 'static + Send>;
enum Message {
ByeBye,
NewJob(Job),
}
struct Worker where
{
_id: usize,
t: Option<JoinHandle<()>>,
}
impl Worker
{
fn new(id: usize, receiver: Arc::<Mutex<mpsc::Receiver<Message>>>) -> Worker {
let t = thread::spawn( move || {
loop {
let message = receiver.lock().unwrap().recv().unwrap();
match message {
Message::NewJob(job) => {
println!("do job from worker[{}]", id);
job();
},
Message::ByeBye => {
println!("ByeBye from worker[{}]", id);
break
},
}
}
});
Worker {
_id: id,
t: Some(t),
}
}
}
pub struct Pool {
workers: Vec<Worker>,
max_workers: usize,
sender: mpsc::Sender<Message>
}
impl Pool where {
pub fn new(max_workers: usize) -> Pool {
if max_workers == 0 {
panic!("max_workers must be greater than zero!")
}
let (tx, rx) = mpsc::channel();
let mut workers = Vec::with_capacity(max_workers);
let receiver = Arc::new(Mutex::new(rx));
for i in 0..max_workers {
workers.push(Worker::new(i, Arc::clone(&receiver)));
}
Pool { workers: workers, max_workers: max_workers, sender: tx }
}
pub fn execute<F>(&self, f:F) where F: FnOnce() + 'static + Send
{
let job = Message::NewJob(Box::new(f));
self.sender.send(job).unwrap();
}
}
impl Drop for Pool {
fn drop(&mut self) {
for _ in 0..self.max_workers {
self.sender.send(Message::ByeBye).unwrap();
}
for w in self.workers {
if let Some(t) = w.t.take() {
t.join().unwrap();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let p = Pool::new(4);
p.execute(|| println!("do new job1"));
p.execute(|| println!("do new job2"));
p.execute(|| println!("do new job3"));
p.execute(|| println!("do new job4"));
}
}