|
|
8 L2 @2 {6 ~& M& p7 F<h1 id="如何实现一个线程池">如何实现一个线程池</h1>2 r! [# _6 o. E
<p>线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,对于计算密集型任务,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。</p>
! B- o2 I5 Y9 n) A* @$ w<p>如何定义线程池Pool呢,首先最大线程数量肯定要作为线程池的一个属性,并且在new Pool时创建指定的线程。</p>
. @5 _! Q5 y% y: g7 @1 O) O<p>线程池Pool</p>
7 F" n7 U& u, ?7 h. W4 z2 B2 b<pre><code>pub struct Pool {% k4 I& O ?2 ^
max_workers: usize, // 定义最大线程数) P/ t- m$ u A' d4 r0 j0 [1 n
} ~( @" E$ P7 `4 E* l* O; z
: L% G' G9 Q: C* fimpl Pool {
' {* v" d) _: h4 | fn new(max_workers: usize) -> Pool {}
# X. v: n& I6 r fn execute<F>(&self, f:F) where F: FnOnce() + 'static + Send {}$ t% d8 ?- r9 f- y7 |& V- B
}8 T9 m6 c) y o1 `9 S
* s- t/ ]4 _1 b1 d, g9 D
</code></pre>. G0 o, o5 ?2 X! Y
<p>用<code>execute</code>来执行任务,<code>F: FnOnce() + 'static + Send</code> 是使用thread::spawn线程执行需要满足的trait, 代表F是一个能在线程里执行的闭包函数。</p>' L6 j8 C4 g' x6 u7 p1 Z9 h
<p>另一点自然而然会想到在Pool添加一个线程数组, 这个线程数组就是用来执行任务的。比如<code>Vec<Thread></code> balabala。这里的线程是活的,是一个个不断接受任务然后执行的实体。<br>) a" h' _% n! J; h& R2 k
可以看作在一个线程里不断执行获取任务并执行的Worker。</p>
# Z! w; S/ k: u- L% ^2 x) W<pre><code>struct Worker where2 H3 G/ F) T6 I* H H
{% `4 h9 U U/ k$ k% s1 s) e- g
_id: usize, // worker 编号4 R3 f; Y& a7 A1 g \1 q
}& ^+ v( h2 `4 ` \4 s
</code></pre>
* @2 R- l& S) ]<p>要怎么把任务发送给Worker执行呢?mpsc(multi producer single consumer) 多生产者单消费者可以满足我们的需求,<code>let (tx, rx) = mpsc::channel()</code> 可以获取到一对发送端和接收端。<br>( f+ w% X- G2 J* T
把发送端添加到Pool里面,把接收端添加到Worker里面。Pool通过channel将任务发送给多个worker消费执行。</p>' w( ^, c; _: e/ A' ]. |& l
<p><strong>这里有一点需要特别注意,channel的接收端receiver需要安全的在多个线程间共享</strong>,因此需要用<code>Arc<Mutex::<T>></code>来包裹起来,也就是用锁来解决并发冲突。</p>
9 Q9 Q1 F4 B! w4 ~- @<p>Pool的完整定义</p>1 n6 v( ^. K6 g" @) E( W6 R
<pre><code>pub struct Pool {
5 P6 C4 y% f2 I) c7 Y- L workers: Vec<Worker>,, G% J |, F5 w3 L! r" [
max_workers: usize,
* @# h K$ ]* S2 c sender: mpsc::Sender<Message>
g F0 u& h' D! o6 p! e}& M! l$ L/ t3 W a2 E4 c6 G
</code></pre>4 F9 A+ Y; n3 ~7 Z a' y8 U2 _2 L
<p>该是时候定义我们要发给Worker的消息Message了<br># w( H' W' R% {" _( Z- L1 W
定义如下的枚举值</p>+ C( j9 F: Y! }5 C k5 b G: Y
<pre><code>type Job = Box<dyn FnOnce() + 'static + Send>;( l9 d# B; Y6 K I+ T
enum Message {
2 D& s1 y* v2 ^" ` ByeBye,
: ^* K* e8 i+ j9 z3 b NewJob(Job),
" x- }% k/ o( Z2 S$ m8 D}6 e: X5 E6 u. f! F
</code></pre>
1 k7 P1 q, i" J<p>Job是一个要发送给Worker执行的闭包函数,这里ByeBye用来通知Worker可以终止当前的执行,退出线程。</p>8 R+ e0 P& H2 t! v( k% c
<p>只剩下实现Worker和Pool的具体逻辑了。</p>7 e1 b+ _) a8 C5 D; O2 }, \3 [9 s
<p>Worker的实现</p>' P9 h$ X8 j, ^' T5 t6 D
<pre><code>impl Worker/ l* b8 f6 i' Y5 r7 c& F$ L
{( W1 X5 }" O t, |/ G
fn new(id: usize, receiver: Arc::<Mutex<mpsc::Receiver<Message>>>) -> Worker {
4 R" B* y( I. @# ]% X C& z let t = thread::spawn( move || {9 \( [8 j. c; X+ y" J1 m4 l5 I- g5 C
loop {/ Y# c6 @8 h2 D3 N+ Y' B& E3 i
let receiver = receiver.lock().unwrap();
9 _; Y# c9 Z% N+ t3 @- ? v let message= receiver.recv().unwrap();; z& Q. [, `+ L* X |3 U
match message {
. [3 N% L& c, ^: l5 ? Message::NewJob(job) => {2 V$ @8 n- @. ^6 q( U4 e( P* A
println!("do job from worker[{}]", id);
6 z; ` J( b p5 p& o job();
: n. q2 ~% B" G& L1 K },+ L& ]1 e$ j, ]& a8 ~" f$ i
Message::ByeBye => {
4 R% Y3 K8 O0 Z println!("ByeBye from worker[{}]", id);
! @: L5 E4 v/ J" z) i# | break/ F! U; t# r5 j2 d8 O& h- {
},
; D' C& S5 a# x, X8 R }
- q6 ]. ?% D0 j# y: C. f }" m H, x. K, U4 P5 v) k
});
6 T0 X/ e! `* Z0 E" h; P0 E% k& e; a) l& T' v8 y
Worker {
; l* @. o" f5 v6 q7 S+ Q _id: id,
. k% A' w5 y8 ~7 H) [+ {8 J! F0 d c t: Some(t),
9 i+ k; }+ ~: j7 P& u }2 R" i9 o1 A# B& H2 \( T, F# b
}8 p7 J/ P5 e8 `
}! K; d- p! r. q- ]' w
</code></pre>" x0 B2 x/ Y; m4 I+ d% g* q
<p><strong>let message = receiver.lock().unwrap().recv().unwrap();</strong> 这里获取锁后从receiver获取到消息体,然后let message结束后rust的生命周期会自动释放掉锁。<br>" S/ {: [% g. @1 W( n; A3 |
但如果写成</p>
}8 u( P" W! ^" h<pre><code>while let message = receiver.lock().unwrap().recv().unwrap() {1 T* C3 q# B2 E+ l$ x6 e" O+ b+ x
};
: h* y7 R6 w9 V, F% o) N; |</code></pre>8 W3 D. X3 Q0 m0 X" d7 ~
<p>while let 后面整个括号都是一个作用域,要在这个作用域结束后,锁才会释放,比上面let message要锁定久时间。<br>
! T+ S |& b& L/ Frust的mutex锁没有对应的unlock方法,由mutex的生命周期管理。</p>
0 H+ O, r2 Z% ^3 \. N+ j<p>我们给Pool实现<code>Drop</code> trait, 让Pool被销毁时,自动暂停掉worker线程的执行。</p>
- \& A0 o E* K5 V<pre><code>impl Drop for Pool {
9 t+ v7 X/ p' Q1 A4 _ fn drop(&mut self) {
" n& e8 e5 i% ^ for _ in 0..self.max_workers {
# p) u, D- J# b4 A self.sender.send(Message::ByeBye).unwrap();" M% B- v' b) V
}2 u( H+ b$ V1 G4 P: z8 Z
for w in self.workers.iter_mut() {
5 Q4 E% a+ i V" h8 e, o: _+ u1 e if let Some(t) = w.t.take() {
! j9 M2 f# q. h1 { t.join().unwrap();5 \8 r2 c# e8 q! ?# B8 ~- p
}$ Z) }3 i% u" V& a; N
}
: q* `) @- U& k( K0 t9 U }
* C9 o* X' d1 s# r% w2 [& W5 |}1 B6 Y0 e3 p. A3 w
4 |8 k1 G% G; T) D z
</code></pre>
. Y; D: F D- P. V& L<p><strong>drop方法里面用了两个循环</strong>,而不是在一个循环里做完两件事?</p>
. o4 r, {3 V8 L: }<pre><code>for w in self.workers.iter_mut() {
5 ?3 T- F& b4 K% \$ ?& E( g: l$ i if let Some(t) = w.t.take() {
4 B3 m& c: t6 Q9 Y- d self.sender.send(Message::ByeBye).unwrap();
8 g/ h m) d/ M. H4 D7 a& F" G t.join().unwrap();" O/ d- M5 M) i, p! x
}5 I0 F3 u( I$ p3 g
}5 \; ?" u7 D/ X# C0 E) N
1 c- @* O( z( Y D; A</code></pre>0 }& l+ U: K6 s- p7 P
<p>这里面隐藏了一个会造成死锁的陷阱,比如两个Worker, 在单个循环里面迭代所有Worker,再将终止信息发送给通道后,直接调用join,<br>$ f6 g7 i- @0 }* ^- z _0 n
我们预期是第一个worker要收到消息,并且等他执行完。当情况可能是第二个worker获取到了消息,第一个worker没有获取到,那接下来的join就会阻塞造成死锁。</p>4 w0 r; M. |- X8 H4 d+ b# }
<p><strong>注意到没有,Worker是被包装在Option内的</strong>,这里有两个点需要注意</p>
% @! t+ O( X) I<ol>
% O+ _ z! S! _) \- J& f; {<li>t.join 需要持有t的所有权</li>. y% h0 J4 R1 i& b- m. e1 o
<li>在我们这种情况下,self.workers只能作为引用被for循环迭代。</li> G6 |: |- P. n8 ?4 B5 w
</ol>
( n% q, d/ O6 c' @ ]5 x<p>这里考虑让Worker持有<code>Option<JoinHandle<()>></code>,后续可以通过在Option上调用take方法将Some变体的值移出来,并在原来的位置留下None变体。<br>
! }1 W) y; p# r) m* I/ o换而言之,让运行中的worker持有Some的变体,清理worker时,可以使用None替换掉Some,从而让Worker失去可以运行的线程</p>) h7 n! w3 o' G$ ]! c1 O
<pre><code>struct Worker where
4 A9 X8 c/ K+ P i. d$ W0 g( z{
K' T. k3 o+ N3 b2 v! X3 O _id: usize,' j8 A7 I. C- f) i
t: Option<JoinHandle<()>>,
* Y3 N6 c$ J# C/ V7 p) }) N3 E}* N5 A/ |6 b# T
</code></pre>- z1 \3 v# m4 }
<h1 id="要点总结">要点总结</h1>( C* `% \1 _: n L
<ul>% s7 R( K: |1 r7 l
<li>Mutex依赖于生命周期管理锁的释放,使用的时候需要注意是否逾期持有锁</li>& U9 b2 S& u9 b9 ^
<li><code>Vec<Option<T>></code> 可以解决某些情况下需要T所有权的场景</li>- P$ l2 N' D w
</ul>: i. f. U3 z2 }! W# P
<h1 id="完整代码">完整代码</h1>* M. z8 l$ U) o8 \$ K" U
<pre><code>use std::thread::{self, JoinHandle};0 g) r( C& Y2 j, m: I* n8 Q( c
use std::sync::{Arc, mpsc, Mutex};0 W( s e7 V" T% F2 O
3 k) F7 g; q; n
$ |" G R) t6 W) z. _4 n) k) Z2 ~type Job = Box<dyn FnOnce() + 'static + Send>;
3 b2 K% _' X! W: |# ~5 z$ H Tenum Message {
8 P9 Y) T% U7 C* c$ `$ d4 S! A ByeBye,
6 E3 @( z4 j$ L- E3 M NewJob(Job),/ V% U1 f. G! u
}
: }% o- s+ c1 X2 `3 g6 w( A) |# d' @" R' E
struct Worker where. H* I# G# o9 {" ~& M( a) g2 X! L8 s
{
# D" w, d( o) u _id: usize,
# ]( o( K* A1 z% v; r9 k. t* ^ t: Option<JoinHandle<()>>,
+ q4 y/ [: T' Y}6 A& e1 O5 h' t3 ]; D$ x
$ \5 U3 N* H s2 _1 t1 f( n6 ]* @4 Gimpl Worker* @& r. [; F# }5 a% A
{
- o# R. k; ^& _3 c% Q6 d& u fn new(id: usize, receiver: Arc::<Mutex<mpsc::Receiver<Message>>>) -> Worker {
0 x3 D( G/ B7 x n& V: k! H9 r5 ` let t = thread::spawn( move || {5 f( F( B( s& E& q0 p
loop {- \ O2 m) |/ X% M
let message = receiver.lock().unwrap().recv().unwrap();
5 n: S( X% _" W* @; {- e- H match message {: s1 n: V$ ]0 Y1 R
Message::NewJob(job) => {
3 B0 g7 P( K. b/ ^3 y' Q println!("do job from worker[{}]", id);' Q6 {7 H: P% t' [; P
job();$ s1 N( D2 d) j, J, R
},
/ m, y4 m/ y3 ?+ a Message::ByeBye => {
$ d6 e) _- Y; ~7 k U3 Y println!("ByeBye from worker[{}]", id);
8 F+ |( g9 w8 h1 N E break; _2 {$ u+ E7 g$ {$ t
},/ b) C/ S, w. z5 r
} 2 K: ~/ `) ^0 J, b
}
/ Q& l$ \% n z$ _1 I3 d. s });
$ k6 y4 }0 A5 B5 V5 a
/ i" k. P& k% k% ]' M8 q+ K, p Worker {/ H- I; Y% X3 \1 g" o
_id: id,0 K( b7 _1 [/ X8 Q- J: Z( H T6 U/ ^
t: Some(t),
7 W# n# @8 I6 c& H6 h }- b# q+ ?3 K- j E- Y9 l/ ^
}
7 e% \; H& O# _, k# [+ [! I, ?}
. o o* Q# \) L' l/ @! `2 m$ q1 |. \9 R: J
pub struct Pool {- }6 [8 i' V, n
workers: Vec<Worker>,7 P) T; @! q0 W- Q
max_workers: usize,6 x$ s; y+ P4 P
sender: mpsc::Sender<Message>
. t1 ~- `* V/ t' Z) y0 e! t7 b: r}
! f6 y! l4 N4 J) e, ]
4 X2 Z4 r6 Z- Jimpl Pool where {
8 Z' q, v( I$ j+ i& l pub fn new(max_workers: usize) -> Pool {' D/ l+ C. |4 H# d* t! D
if max_workers == 0 {' Z8 k, d* X/ a6 Z3 @7 }5 V
panic!("max_workers must be greater than zero!")# w& f7 U# Y$ n) o& g4 [: T R5 u
}6 D) ]1 H4 W; j' K9 g
let (tx, rx) = mpsc::channel();
% G8 q( U5 V: t( ]' m: z- _" P) O
, ? a- n+ g! U8 k n5 ? let mut workers = Vec::with_capacity(max_workers);% b1 F/ M* Q2 _2 x. p: p; F
let receiver = Arc::new(Mutex::new(rx));& o" o& J4 m! d
for i in 0..max_workers {
% D- d# T9 Y2 G$ b9 p& d8 C# v workers.push(Worker::new(i, Arc::clone(&receiver)));9 o# ?- i. m+ u: Z9 s( s1 N
}; G; ?+ I' K+ m
- w" V3 F3 ~ a0 j3 | t) q$ l! J& z Pool { workers: workers, max_workers: max_workers, sender: tx }
: [& [7 T( x' I4 N, x }
" s1 t9 \4 D2 A! `0 S2 g' l # y2 r( ~7 | d+ S* `
pub fn execute<F>(&self, f:F) where F: FnOnce() + 'static + Send
6 d% b7 d5 i" ]& _ {- y1 _$ x! o. \$ g( d/ K* `8 \% b
^; _4 T) ?3 O. ]7 H/ ^ let job = Message::NewJob(Box::new(f));
+ ]) `$ a- H7 Y9 m self.sender.send(job).unwrap();
, x9 W5 S7 |7 C) L; s7 N4 b8 N }, y( m2 B8 q" N( {% Q
}5 ~5 l; ^: U+ j8 \' R8 p% d, O
3 N3 z; ]; l9 |- L5 t& f: Uimpl Drop for Pool {
3 z: z# ?0 _5 `. P: u9 d* o fn drop(&mut self) {
( h \6 s' Q$ ^* e2 _ for _ in 0..self.max_workers {( y/ I/ e" c1 I M3 w2 U& L
self.sender.send(Message::ByeBye).unwrap();
0 z2 P/ f, s$ z) F9 v: c }
( L: ^" Z1 n. e# n for w in self.workers {
5 `( D3 o$ f5 O; A; Z if let Some(t) = w.t.take() {
' {7 Y, F( J" D; w6 l; D* k$ f t.join().unwrap();& A6 u. X; F; u0 g
}
. ~9 B0 v L9 j X; z$ E }
/ z! d2 K9 F ]9 ~. _7 P. a+ Z }
- @6 A6 b6 A7 ], i7 i8 @} d. w, U2 R! R3 E$ ]7 q4 [
' @8 \( l) d% H( w: X) P' \: X
' ?4 l& @9 b; q' _#[cfg(test)]( f k# Z+ [: B; L8 O. a4 i
mod tests {8 j; X5 _" C7 f0 ]$ t6 O
use super::*;) I0 K! y( z! a( X, P+ ^ S5 O
#[test]
, a5 [7 W+ A; i+ b4 w fn it_works() {
& Y+ F6 ?7 i* a* N# ]( Y3 L/ X let p = Pool::new(4);
, H, {2 l8 [$ ~' h- E+ p p.execute(|| println!("do new job1"));
0 \- Z# d, X! [7 M! N& f8 x p.execute(|| println!("do new job2"));. N- O, v2 l0 h) y: H
p.execute(|| println!("do new job3"));% l" M* ]7 p& D5 P1 w" \0 Z
p.execute(|| println!("do new job4"));
7 p" Q2 |0 f% l& @7 @ }3 R( T4 ^1 m" D" w/ ?" ^
}/ K8 Q! r: T- _3 u
</code></pre>0 j- R T4 n3 D- A
1 z( t$ s$ u9 G& y) k* T" y |
|