文 Akisann@CNblogs / zhaihj@Github

本篇文章同时发布在Github上:https://zhaihj.github.io/writing-a-threadpool-in-rust.html

多线程一直是我相当不想碰的东西,总觉得看起来很棒,用起来却一点都不放心——尤其是过去用Delphi体验了多线程之后。实际上到了多线程里根本就没法定位那里出了错误,因此大部分时间压根不是在“调试”,而是告诉用户怎么用才能避免这个错误。在给OOC写MultiTheard Generating的时候我也紧紧用了最简单的ThreadPool,并且因为互斥锁多线程还没有单线程快。总之,这么多年我一直有意的躲避多线程的问题。

而Rust一直在宣传它在多线程上的优势——所有权如何的好,在Servo里面他们如何用Rust写了个漂亮的Parallel Parser。加上最近一年一直再用Rust写游戏的服务器,于是我决定回头看看Rust下的多线程体验到底如何。

ThreadPool

相比简单的用多线程算个加法,或者写个The Computer Language Benchmarks Game的测试程序来说,ThreadPool可能更适合练手。在这里,我会实现下面这个结构的ThreadPool:

简单来说,主线程通过Channel向ThreadPool发送Job,然后Threadpool里的每一个子线程在空闲时都会尝从Channel里获取Job,然后执行它。执行完毕之后,Job的返回结果会通过另一个Channel传送给主线程。实际上,在C或Delphi里,实现这么一个东西似乎并不是很困难,然而,Rust因为独特的所有权制度,代码比思路要复杂一些,下面,让我们来看看怎么实现这个ThreadPool。

Jobs for the Job

首先,让我们来设计Job,在这篇文章里,Job总是一个没有参数的函数,用代码来说,是这样:

type Job<T> = Box<Fn() -> T + Send + 'static>

由于我们需要把返回值传回给主线程,因此Job的返回值需要Send属性。这里,没有用FnOnce而是Fn仅仅是因为目前Rust的Box并不支持Box(),并且我并不打算用BoxFn来增加文章的复杂度。

对于每一个线程,Job大概是这样使用的:

loop {
if let Ok(f) = get_a_job() {
send_to_main_thread(f());
}
}

我们每个线程都会不停的尝试获取job,执行它,然后获取下一个。相信到这里已经有不少人发现了问题:这个子线程似乎永远都无法结束。因为get_a_job显然只能返回Ok或阻塞——如果当Channel为空它就出错的话,一旦没有新任务,所有的子线程都会退出,之后这个ThreadPool就再也没法用了。于是,为了让这个子线程能退出,我们给Job添加一个用来表示结束的状态:

enum Message<T> {
Work(Box<Fn() -> T + Send + 'static),
Terminate,
} type Job<T> = Message<T>;

这样,一旦Job是Terminate,线程就知道ThreadPool已经准备退出了。于是,我们可以把子线程写成这样:

loop {
if let Ok(f) = get_a_job() {
match f {
Message::Work(f) => f(),
Message::Terminate => break,
}
}
}

有了这个定义之后,让我们来看看ThreadPool该怎么设计。

从前一节的图片里,我们知道这个ThrealPolo需要两个Channel,一个用来接受新Job,另一个用来发送Job的返回结果,因此,ThrealPool可以写成这样:

struct ThreadPool <T>{
sender: mpsc::Sender<Job<T>>,
pub result: mpsc::Receiver<T>,
threads: Vec<Option<thread::JoinHandle<()>>>,
}

这里,我们用了std::sync::mpsc,而threads则是用来储存所有子线程的,毕竟在结束的时候我们还要关闭他们的句柄。下面,我们尝试生成这个ThreadPool。由于子线程数是静态的,几乎所有的工作都可以在new里面完成,我们的大致思路是这样:

  • 生成两个Channel, A和B,A用来接受Job,B用来发送结果
  • 生成n个线程,每一个线程里:
  • 保留A的Receiver和B的Sender
  • 通过A的Receiver接受Job,执行,通过Sender发送

看起来并不是很难,让我们把它变成代码:

impl<T> ThreadPool<T> where T: Send + 'static { 

    ....

    // n是线程数,s是每一个线程获取新Job时的等待时间
fn new(n: usize, s: u64) -> Self {
// 用来接收Job的channel
let (tx, rx) = mpsc::channel();
// 用来发送结果的Channel
let (tx1, rx1) = mpsc::channel(); let rx : Arc<Mutex<mpsc::Receiver<Job<T>>>> = Arc::new(Mutex::new(rx));

首先我们定义了必要的channel,需要注意的是,对于channel,通常我们认为Sender可以有多个,但Receiver只有一个,因此在设计时Sender可以自然应对多线程,但Receiver则不。对于我们这里的情况,可以手动对Receiver加一个互斥锁,在Rust里可以使用std::sync::Mutex

下面就是生成n个线程了:

let v = (0 .. n).map(| _ | {
let rx = rx.clone();
let tx1 : mpsc::Sender<T> = tx1.clone();
Some(thread::spawn(move ||
loop {
if let Ok(f) = rx.lock() {
if let Ok(f) = f.recv() {
match f {
Message::Work(f) => {
let r : T = f();
tx1.send(r);
},
Message::Terminate => break,
}
}
}
}
))
}).collect::<Vec<_>>();

这里,thread::spawn用来生成新的线程,每个线程所作的事情就如之前描述的一样。最后,把这一切合起来,就可以得到一个ThreadPool了:

ThreadPool {
sender: tx,
result: rx1,
threads: v,
}

有了这个ThreadPool之后,我们还需要一个方法来随时向里添加新Job,不过这件事情非常简单——只需要通过sender发送就够了:

fn add(&self, f: Job<T>) {
self.sender.send(f).unwrap();
}

为了简单,我们并没有处理send返回的错误,在实际应用里,add可以返回Result

Finishing and Drop

实际上,到此为止,大部分工作已经结束了,最后还有一个小事情:ThreadPool没法自己结束,因此我们需要手动实现这一部分。在其他语言里析构可能是理所当然的,不过在Rust里,除了C/C++的Wrapper之外,这种事情并不常见。Rust提供了Drop Trait来实现析构,Drop里的drop函数会在内存释放前自动执行。因此,我们只需要这么做:

impl<T> Drop for ThreadPool<T> {
fn drop(&mut self) {
for _ in &self.threads{
self.sender.send(Message::Terminate);
}
for t in &mut self.threads {
if let Some(t) = t.take() {
t.join().unwrap();
}
}
}
}

在这里,我们先对每一个线程发送结束命令,然后等待他们结束(join)。在实际应用里,线程可能会因为Job比较耗时而无法处理Terminate命令,Drop也会卡在Join的部分,不过可惜的是Rust的Thread并没有提供non-blocking的方法,因此你可能需要Future-rs来实现non-blocking的join。

Try It

到这里,主要部分已经完成了,下面我们来测试一下这个ThreadPool的效果如何:

fn main() {
let tp = ThreadPool::new(10, 100);
for i in 0 .. 100 {
tp.add(
Message::Work(
Box::new(move || { println!("Thread: {}", i); i*100 })));
}
let mut c = 0;
loop {
if let Ok(s) = tp.result.recv() {
println!("Result: {}", s);
c += 1;
}
if c == 100 {
break;
}
}
}

线程池有10个子线程,每个Job会输出一行文字,并返回一个数字,编译执行后,结果看起来像这样:

Thread: 0
Thread: 1
Result: 0
Result: 100
Thread: 2
Thread: 3
Result: 200
Result: 300
Thread: 4
Thread: 5
Thread: 6
Result: 400
Result: 500
Result: 600
......

Conclusion

在文章里,我尽量平白的描述这个过程,不过如果在没有基础的情况下自己去写的话,可能会遇到很多有趣的问题,比如:实际上thread.join会消耗掉JoinHandle的,因此,如果用Vec<JoinHandle>来储存线程句柄的话,会无法编译通过——因为drop是by ref的。在这篇文章里,我用了跟The Book同样的方法:添加一个Option来解决这个问题。不过,The Boox并不代表着最好的解决办法,比如我们还可以:

while let Some(e) = self.threads.pop() {
e.join().unwrap();
}

对我来说,这个办法看起来更加简洁和优雅——但不知为什么The Book没有这么做。

本文的源代码可以在我的Github里找到。

Writing A Threadpool in Rust的更多相关文章

  1. Fast + Small Docker Image Builds for Rust Apps

    转自:https://shaneutt.com/blog/rust-fast-small-docker-image-builds/ In this post I’m going to demonstr ...

  2. Linux内核学习资料

    1.为什么计算机的学生要学习Linux开源技术 http://tinylab.org/why-computer-students-learn-linux-open-source-technologie ...

  3. rust 实战 - 实现一个线程工作池 ThreadPool

    如何实现一个线程池 线程池:一种线程使用模式.线程过多会带来调度开销,进而影响缓存局部性和整体性能.而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务.这避免了在处理短时间任务时创建与销毁线 ...

  4. Why use async requests instead of using a larger threadpool?(转载)

    问: During the Techdays here in the Netherlands Steve Sanderson gave a presentation about C#5, ASP.NE ...

  5. [WASM + Rust] Debug a WebAssembly Module Written in Rust using console.log

    Having some kind of debugging tool in our belt is extremely useful before writing a lot of code. In ...

  6. [WASM] Set up wasm-bindgen for easy Rust/JavaScript Interoperability

    Interoperability between JavaScript and Rust is limited to numerics and accessing memory directly. S ...

  7. Rust入坑指南:齐头并进(下)

    前文中我们聊了Rust如何管理线程以及如何利用Rust中的锁进行编程.今天我们继续学习并发编程, 原子类型 许多编程语言都会提供原子类型,Rust也不例外,在前文中我们聊了Rust中锁的使用,有了锁, ...

  8. Rust Aya 编写 eBPF 程序

    本文地址:https://www.ebpf.top/post/ebpf_rust_aya 1. 前言 Linux 内核 6.1 版本中有一个非常引人注意的变化:引入了对 Rust 编程语言的支持.Ru ...

  9. Spring Enable annotation – writing a custom Enable annotation

    原文地址:https://www.javacodegeeks.com/2015/04/spring-enable-annotation-writing-a-custom-enable-annotati ...

随机推荐

  1. 京东JOS API 接入使用笔记

    商户开设了京东店.淘宝店,最近打算使用京东物流,需要使用京东仓库(京东店的订单使用京仓发货,淘宝等其他店使用京东云仓)发货,所以得从自家的ERP与京东沧海(ECLP)API对接,实现收发存. 首先得在 ...

  2. C# 调用.exe文件

    process da = new process(); da.startinfo.filename = @""D:\BM0002\BM0002.exe";  //要调用的 ...

  3. Linux最小化安装

    1,linux安装网络自动配置: 2,linux硬盘分配 1,/boot 用来存放与 Linux 系统启动有关的程序,比如启动引导装载程序等,建议大小为 100-200MB . 2,swap 实现虚拟 ...

  4. GitHub 入门教程

    一.前言 编程进阶的道路是坎坷的,没有任何捷径.这个时期只能是积累.吸收.学习.坚持,做到量的积累,到质的飞跃 古语有云:'书山有路,勤为径'.'不积跬步,无以至千里' 编程是一个动手实践性的学科,多 ...

  5. spring默认欢迎页设置

    简单配置的方式,直接展示静态网页,不经过Controller. web.xml 中什么没有配置任何有关欢迎页的信息!其实这时等效于如下配置:这个会由Web容器最先访问! //-未指定欢迎页时,缺省等于 ...

  6. 百度地图JavaScript API使用

    最近在完成优达学城前端开发(入门)课程的P4项目中,要求调用google地图进行交互,项目已提供部分js代码和html代码.但在申请google地图API密钥时由于网络等原因,打不开或者连接超时,所以 ...

  7. ubuntu上安装nginx+mysql+php5-fpm(PHP5 - FastCGI Process Manager)

    题外话:由于近段时间测试环境ssh链路质量不大好,经常短线.故我把整个安装过程放到screen里去执行,以防止断线中断了安装过程.执行screen -S install,这样断线后,只要再执行scre ...

  8. 如何在github制作一个网页

    1.首先得先注册一个github账号,官网:https://github.com/ 2.注册完,登录账号进入首页,点右上角的 ‘+’ 创建新的仓库 3. 点击setting,选择一个主题, 4. 选完 ...

  9. 基于angular2x+ng-bootstrap构建后台管理系统界面(干货)

    写在前面的话 近来公司要做一个后台管理系统,人手比较少,于是作为一个前端也参与进来,其实据我所知,大部分的公司还是后台自己捣鼓的. 在后台没有到位的情况下,前端应该使用什么技术也着实让我为难了一把.经 ...

  10. Java之线程,常用方法,线程同步,死锁

    1, 线程的概念 进程与线程 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程.(进程是资源分配的最小单位) 线程:同一类线程共享代码和数据 ...