rust 实战 - 实现一个线程工作池 ThreadPool
如何实现一个线程池
线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络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"));
}
}
rust 实战 - 实现一个线程工作池 ThreadPool的更多相关文章
- 线程池ThreadPool
在面向对象编程中,经常会面对创建对象和销毁对象的情况,如果不正确处理的话,在短时间内创建大量对象然后执行简单处理之后又要销毁这些刚刚建立的对象,这是一个非常消耗性能的低效行为,所以很多面向对象语言中在 ...
- [JCIP笔记] (三)如何设计一个线程安全的对象
在当我们谈论线程安全时,我们在谈论什么中,我们讨论了怎样通过Java的synchronize机制去避免几个线程同时访问一个变量时发生问题.忧国忧民的Brian Goetz大神在多年的开发过程中,也悟到 ...
- 线程池ThreadPool实战
线程池ThreadPool 线程池概念 常用线程池和方法 1.测试线程类 2.newFixedThreadPool固定线程池 3.newSingleThreadExecutor单线程池 4.newCa ...
- 二 Java利用等待/通知机制实现一个线程池
接着上一篇博客的 一Java线程的等待/通知模型 ,没有看过的建议先看一下.下面我们用等待通知机制来实现一个线程池 线程的任务就以打印一行文本来模拟耗时的任务.主要代码如下: 1 定义一个任务的接口 ...
- 线程池ThreadPool知识碎片和使用经验速记
ThreadPool(线程池)大概的工作原理是,初始时线程池中创建了一些线程,当应用程序需要使用线程池中的线程进行工作,线程池将会分配一个线程,之后到来的请求,线程池都会尽量使用池中已有的这个线程进行 ...
- 线程池ThreadPool的初探
一.线程池的适用范围 在日常使用多线程开发的时候,一般都构造一个Thread示例,然后调用Start使之执行.如果一个线程它大部分时间花费在等待某个事件响应的发生然后才予以响应:或者如果在一定期间内重 ...
- [转]使用VC/MFC创建一个线程池
许多应用程序创建的线程花费了大量时间在睡眠状态来等待事件的发生.还有一些线程进入睡眠状态后定期被唤醒以轮询工作方式来改变或者更新状态信息.线程池可以让你更有效地使用线程,它为你的应用程序提供一个由系统 ...
- 多线程系列 线程池ThreadPool
上一篇文章我们总结了多线程最基础的知识点Thread,我们知道了如何开启一个新的异步线程去做一些事情.可是当我们要开启很多线程的时候,如果仍然使用Thread我们需要去管理每一个线程的启动,挂起和终止 ...
- C#线程池ThreadPool的理解
在多线程编程中,线程的创建和销毁是非常消耗系统资源的,因此,C#引入了池的概念,类似的还有数据库连接池,这样,维护一个池,池内维护的一些线程,需要的时候从池中取出来,不需要的时候放回去,这样就避免了重 ...
随机推荐
- [特别篇] 记JZ冬令营(Finished)
1.16 走错班了, 去了全是大佬的1班, 然后灰溜溜滚回2班了. 去参加开营仪式. 然而昏昏欲睡... 实在太累了澡也没洗.. 群英云集, 多是感慨. 当时依依惜别和铮铮誓言, 在重逢中无语凝噎. ...
- 分别使用time 和 datetime模块记录当前时间
工作中经常混淆这两种方法 现记录一下 加深印象 代码如下: >>> import time>>> import datetime>>> ct1 = ...
- Git_使用SSH密钥操作远端仓库
git支持多种传输协议,ssh协议是其中一种. 初次使用git的用户要使用ssh协议大概需要三个步骤: 生成密钥 设置远程仓库(本文以github为例)上的公钥 把git的 remote url 修改 ...
- python 单下划线与双下划线的区别
来自为知笔记(Wiz)
- CentOS7端口被占用的解决办法
1.根据端口号得到其占用的进程的详细信息 netstat -tlnp|grep 80tcp 0 0 192.168.33.10:80 0.0.0.0:* ...
- 实验 6 :OpenDaylight 实验——OpenDaylight 及 Postman实现流表下发
实验 6 :OpenDaylight 实验--OpenDaylight 及 Postman实现流表下发 一.实验目的 熟悉 Postman 的使用:熟悉如何使用 OpenDaylight 通过 Pos ...
- java集合【13】——— Stack源码分析走一波
前言 集合源码分析系列:Java集合源码分析 前面已经把Vector,ArrayList,LinkedList分析完了,本来是想开始Map这一块,但是看了下面这个接口设计框架图:整个接口框架关系如下( ...
- .NET下如何拦截鼠标、键盘消息?Win32NET来帮你
Win32NET是一个Win32API的.NET下封装的类库,包含: 1: 常用win32的API的net封装 2:鼠标.键盘.热键hook钩子模块, 3:模拟键盘输入文字(支持各种字符文字.不同语言 ...
- JAVA实现对阿里云DNS的解析管理
1.阿里云DNS的SDK依赖 <dependency> <groupId>com.aliyun</groupId> <artifactId>tea-op ...
- 【刷题-LeetCode】123 Best Time to Buy and Sell Stock III
Best Time to Buy and Sell Stock III Say you have an array for which the ith element is the price of ...