使用Rust构建可以并发执行多个任务的线程池

开发 前端
线程池是工作线程的集合,创建这些线程是为了同时执行多个任务并等待新任务的到来。这意味着一开始创建了多个线程,并且所有线程都处于空闲状态。

在这篇文章中让我们探讨一下如何使用Rust构建线程池来并发地管理多个任务。

在开始实际的编码之前,让我们首先了解线程池是什么以及它是如何工作的。

线程池

线程池是工作线程的集合,创建这些线程是为了同时执行多个任务并等待新任务的到来。这意味着一开始创建了多个线程,并且所有线程都处于空闲状态。

每当你的系统获得任务时,它可以快速地将任务分配给这些线程,从而节省大量时间,而无需多次创建和删除线程。

图片图片

正如图所看到的,线程池是等待从主线程接收任务以执行的多个线程的集合。

在该图中,主线程中总共有15个任务,所有这些任务都被转发给不同的工作线程并发执行。了解了线程池的概念后,让我们来理解线程池的内部工作原理。

线程池是如何工作的?

在线程池体系结构中,主线程只有两个任务:

1,接收所有的任务并将它们存储在一个地方。

2,创建多个线程,并定期为它们分配不同的任务。

在接收任务之前创建线程集,并使用ID存储在某个地方,以便我们可以通过ID识别它们。

然后每个线程都在等待接收任务,如果它们得到任务,就开始处理任务。完成任务后,他们再次等待下一个任务。

当该线程忙于执行任务时,主线程将更多的任务分配给其他线程,这样在主线程结束任务之前没有线程空闲。在完成所有任务后,主线程终止所有线程并关闭线程池。

现在我们了解了线程池是如何工作的。接下来,让我们使用Rust实现一个线程池。

使用Rust实现线程池

1. 创建线程

我们需要一个函数来生成一个线程并返回它的JoinHandle。

此外,我们需要知道线程的ID,如果我们搞砸了,就可以用线程ID记录错误,这样我们就可以知道哪个线程出错了。

可以看出,如果两个相互关联的数据需要组合,需要一个结构体。我们来创建一个:

struct Worker {
    id: usize,
    thread: JoinHandle<()>
}

现在我们实现一个可以返回新Worker的构造函数:

impl Worker {
    fn new(id: usize) -> Self {
        let thread = thread::spawn(|| {});

        Self {id, thread}
    }
}

现在,我们的函数已经准备好创建线程并将它们返回给调用者。

2. 存放线程

我们需要一个结构来保存所有线程的所有JoinHandles,我们还想控制线程池可以拥有多少线程。

这意味着,我们需要一个带有构造函数的结构体,该函数指定一个数字来指示线程的数量,并且必须调用Worker来创建线程。

struct ThreadPool {
    workers: Vec<Worker>,
}

impl ThreadPool {
    fn new(size: usize) -> Self {
        assert!(size > 0, "Need at least 1 worker!");

        let mut workers = Vec::with_capacity(size);

        for i in 0..size {
            workers.push(Worker::new(i));
        }

        Self { workers }
    }
}

我们有了创建线程和管理线程的函数,现在是时候创建一个可以将任务分配给不同线程的函数了。

3. 给线程分配任务

我们的线程池结构体必须有一个函数,该函数可以在线程内部分配和执行任务。但是有一个问题,我们如何将任务发送给线程,以便线程能够执行任务?

为此,我们需要一个task类型来表示我们需要完成的任务:

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

在这里,意味着我们的任务必须实现Box<dyn>里的这些Trait:

1,实现FnOnce()意味着我们的任务是一个只能运行一次的函数。

2,实现Send,因为我们将任务从主线程发送到工作线程,所以将任务设置为Send类型,以便它可以在线程之间安全地传输。

3,实现'static,意味着我们的任务必须和程序运行的时间一样长。

现在是时候将任务发送给工作线程了,但要做到这一点,我们必须在主线程和所有工作线程之间建立一个通道,因此我们需要使用Arc<Mutex<()>>。

让我们来更新这两个构造函数:

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

impl ThreadPool {
    fn new(size: usize) -> Self {
        assert!(size > 0, "Need at least 1 worker!");

        let (sender, reciever) = mpsc::channel();
        let reciever = Arc::new(Mutex::new(reciever));

        let mut workers = Vec::with_capacity(size);

        for i in 0..size {
            workers.push(Worker::new(i, Arc::clone(&reciever)));
        }

        Self {
            workers,
            sender: Some(sender)
        }
    }
}

impl Worker {
    fn new(id: usize, reciever: Arc<Mutex<Receiver<Task>>>) -> Self {
        let thread = thread::spawn(move || {});

        Self {
            id,
            thread
        }
    }
}

在ThreadPool构造函数中,我们创建了一个新的通道,并在Arc<Mutex<()>>中封装了接收器,我们把接收器发送给工作线程,以便主线程可以发送任务,工作线程可以接收任务。

此外,我们必须更新ThreadPool结构体,以包含一个发送者,它将被主线程用来向不同的线程发送任务。

现在,让我们实现在工作线程中执行任务的逻辑:

fn new(id: usize, reciever: Arc<Mutex<Receiver<task>>>) -> Self {
    let thread = thread::spawn(move || {
        loop {
            let receiver = reciever.lock()
                .expect("Failed to grab the lock!")
                .recv();

            match receiver {
                Ok(task) => {
                    println!("Thread {} got the task& executing.", id);
                    task();
                    thread::sleep(Duration::from_millis(10));
                },

                Err(_) => {
                    println!("No got the task");
                    break;
                }
            }
        }
    });

    Self {
        id,
        thread
    }
}

这里,在每个循环中,我们都试图获得锁并调用锁上的recv(),以便我们可以获得主线程发送的任务。

接下来,我们在ThreadPool中实现一个函数,将任务发送到不同的线程。

impl ThreadPool {
    fn new(size: usize) -> Self {
        // snip
    }

    fn execute<F>(&self, job: F)
    where
        F: FnOnce() + Send + 'static
    {
        let job = Box::new(job);

        self.sender.send(job)
            .expect("Failed to send the job to workers!");
    }
}

我们还需要创建一个函数,在ThreadPool结束时动态终止所有线程。简单地说,我们必须手动实现ThreadPool的Drop特性,在那里我们将终止所有线程。

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Thread {} is shutting down.", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join()..unwrap_or_else(|_| panic!("Failed to join the thread {}", worker.id));}
        }
    }
}

这里我们还必须删除发送方,因为如果我们不这样做,那么接收方将永远循环。如果删除发送者,那么接收者也会自动删除,我们就可以成功地退出这个程序。

测试

main函数代码如下:

fn main() {
    let pool = ThreadPool::new(5);

    for _ in 0..10 {
        pool.execute(|| println!("Doing something"));
    }
}

运行结果:

Thread 0 is shutting down.
Thread 0 got the job & executing.
Doing something
Thread 3 got the job & executing.
Doing something
Thread 1 got the job & executing.
Thread 2 got the job & executing.
Doing something
Thread 4 got the job & executing.
Doing something
Doing something
Thread 0 got the job & executing.
Doing something
Thread 4 got the job & executing.
Doing something
Thread 3 got the job & executing.
Doing something
Thread 2 got the job & executing.
Doing something
Thread 1 got the job & executing.
Doing something
No got the job
Thread 1 is shutting down.
No got the job
No got the job
No got the job
No got the job
Thread 2 is shutting down.
Thread 3 is shutting down.
Thread 4 is shutting down.


责任编辑:武晓燕 来源: coding到灯火阑珊
相关推荐

2022-03-28 08:31:29

线程池定时任务

2021-02-06 14:02:55

线程池Builder模式

2023-08-04 11:04:03

线程池项目开发

2023-07-05 07:48:04

线程池join关闭状态

2022-04-26 08:41:38

Swift并发系统iOS

2023-12-29 09:38:00

Java线程池

2020-09-04 10:29:47

Java线程池并发

2020-12-10 07:00:38

编程线程池定时任务

2017-01-10 13:39:57

Python线程池进程池

2022-11-09 09:01:08

并发编程线程池

2019-08-16 11:16:25

Java程序员流程图

2022-09-29 09:19:04

线程池并发线程

2022-03-30 08:54:21

线程 Thread判断线程池任务Java

2023-11-29 16:38:12

线程池阻塞队列开发

2021-09-11 15:26:23

Java多线程线程池

2024-10-14 13:12:59

2024-11-06 09:39:52

2024-06-20 13:59:26

2020-02-17 16:28:49

开发技能代码

2024-09-13 09:55:38

RustP2P网
点赞
收藏

51CTO技术栈公众号