如何在Rust中实现一个线程池。

330 阅读6分钟

大家好,我是梦兽。一个 WEB 全栈开发和 Rust 爱好者。如果你对 Rust 非常感兴趣,可以关注梦兽编程公众号获取群,进入和梦兽一起交流。

线程池(Thread Pool)是一种用于管理和复用线程的技术,它能够有效地提高多线程应用程序的性能和资源利用率。线程池的主要目的是减少线程创建和销毁的开销,以及控制并发线程的数量,从而避免系统资源被过度消耗。

我们要如何实现一个线程池呢?

我们可以使用工作池模式来并发执行任务。 首先,让我们创建一个Worker结构体:

struct Worker {
  #[allow(dead_code)]
  id: usize,
  thread: JoinHandle<()>
}

一个工作线程将代表一个单独的线程,所以有多少个工作线程就意味着可以同时进行多少个任务。我们给Worker结构体两个字段,第一个是id,类型是usize,第二个是thread,类型是JoinHandle<()>,这个JoinHandlethread::spawn函数的返回类型,每个工作线程都将持有一个线程,并且我们还可以在这个线程上调用join方法来等待每个线程完成。

其次,我们创建一个类型别名Job:

type Job = Box<dyn FnOnce() + Send + 'static>;

这是一个用于闭包的类型,我们可以通过通道发送和接收任务,并且还可以在thread::spawn函数中执行它。我们只会调用它一次,所以给它FnOnce();我们将在线程之间发送它,所以会给它Send;最后给它一个’static的生命周期。为了使它成为一个类型,我们将它们放在一个盒子中,这样它就变成了一个特征对象类型。

第三,让我们定义一个WorkerPool结构体:

struct WorkerPool {
  workers: Vec<Worker>,
  sender: mpsc::Sender<Job>
}

我们为这个结构体添加了两个字段,一个是用于存储工作线程的向量,第二个是来自通道的发送者,这样我们就可以使用这个发送者将任务发送给工作线程,以便工作线程可以处理这个任务。所以这个发送者字段的类型是一个持有Job的Sender。

第四,我们为Worker结构体实现了一个新的函数来创建Worker的新实例:

impl Worker {
    fn new(id: usize , receiver: Arc<Mutex<Receiver<Job>>>) -> Worker {
      let thread = thread::spawn(move || loop {
        let job = receiver.lock().unwrap().recv().unwrap();
        println!("worker id : {} , get a job" , id);
        job()
      });
      Self { id, thread }
    }
}

这个新函数接受两个参数,一个是usize类型的id,第二个是一个接收者,因为我们将在这个接收者上跨线程共享,这个接收者用于接收一个Job,所以使用Arc持有Mutex持有带有Job的Receiver。这个新函数的返回类型是Worker。

在函数体中,我们调用thread::spawn来创建一个新线程,我们在spawn中的闭包中添加了move关键字,并在闭包体的块中添加了循环,所以这个线程将始终运行以等待接收一个任务。我们调用lock来锁定互斥锁以获取接收者,然后解开锁的结果,接着在接收者上调用recv方法来接收一个任务,然后调用unwrap来处理recv方法的结果。我们打印哪个工作线程id获得了一个任务。然后调用job闭包。

最后,我们返回一个具有id和JoinHandle<()>线程的Worker实例。

第五,我们在WorkerPool上实现方法:

impl WorkerPool {
    fn new(size: usize-> Self {
      // 使用channel模型可以省掉很多不必要的操作,费一点内存是值得的。
      let (sender, receiver) = channel();
      let mut workers = vec![];
      let receiver = Arc::new(Mutex::new(receiver));
      for i in 1 ..=size {
        let receiver =  Arc::clone(&receiver);
        workers.push(
          Worker::new(i, receiver)
        );
      };
      WorkerPool { 
        workers,
        sender
      }
    }
    fn execute<F>(&self, f: F)
    where 
      F: FnOnce() + Send + 'static
    {
      let job = Box::new(f);
      self.sender.send(job).unwrap();
    }
    fn join(self) {
      for worker in self.workers {
        worker.thread.join().unwrap()
      }
    }
}

1

我们实现了一个新的函数来创建WorkerPool的新实例,这个新函数我们添加了一个参数size,其类型是usize整数。它将决定这个池中有多少个工作线程。这个函数返回一个WorkerPool的实例。

在新函数体中,我们创建了一个新的通道,我们将使用这个通道将任务发送给工作线程。我们创建了一个可变的空向量来存储工作线程。我们将接收者放在Arc中,这样我们就可以在跨线程安全地共享这个接收者。我们使用for循环来创建每个新的工作线程,并给它一个id,并调用Arc::clone来创建对接收者的共享引用。我们将新的工作线程推入我们之前创建的向量中。

最后,我们返回带有工作线程和发送者的WorkerPool实例。

2

我们创建了一个execute方法来从WorkerPool发送任务到工作线程,这个方法将f作为第二个参数,其类型是FnOnce() + Send + 'static,所以FnOnce意味着它只能被调用一次,Send可以在线程之间发送,生命周期是’static。

在execute方法体中,我们将这个f放入一个盒子中,然后我们发送这个f。

3

我们创建了一个名为join的方法,它接受一个self参数。我们将使用这个方法来在每个工作线程的JoinHandle上调用join方法,这样它将等待线程完成工作。

在join方法体中,我们使用for循环来遍历每个工作线程,并在每个线程上调用join方法。

使用教程

fn main() {
  let pool = WorkerPool::new(2);
  pool.execute(hello);
  pool.execute(world);
  pool.execute(hello1);
  pool.join();
}

fn hello () {
  thread::sleep(Duration::new(10));
  println!("hello")
}

fn world() {
  println!("world")
}

fn hello1() {
  thread::sleep(Duration::new(20));
  println!("hello")
}

我们创建了三个函数,其中两个函数将调用sleep函数以阻塞。在main函数中,我们调用new函数来创建一个包含2个工作线程的工作池,这样这个工作池可以并发执行两个任务。我们调用execute方法来运行hello、world和hello1函数。即使hello函数会睡眠1秒,但world函数不会被阻塞,它将立即执行,然后在世界函数完成后,工作线程将执行hello1函数。最后,我们在池上调用join方法来等待所有任务完成。

注意

感谢阅读!感谢您的时间,并希望您觉得这篇文章有价值。如果你有更好的建议可以下面的评论中告诉我它如何改善了您的编码体验!

创建和维护这个博客以及相关的库带来了十分庞大的工作量,即便我十分热爱它们,仍然需要你们的支持。或者转发文章。通过赞助我,可以让我有能投入更多时间与精力在创造新内容,开发新功能上。赞助我最好的办法是微信公众号看看广告。

本文使用 markdown.com.cn 排版