To get started, ensure you have Rust and Cargo installed on your machine. You can install the tokio crate by adding it to your Cargo.toml file:

[dependencies]
tokio = { version = "1", features = ["full"] }

Step 1: Define the Task Structure

We will begin by defining a structure to represent a task. Each task will contain a function to execute and a delay before execution.

use std::time::Duration;
use tokio::time::sleep;

struct Task {
    delay: Duration,
    action: Box<dyn Fn() + Send + 'static>,
}

impl Task {
    fn new<F>(delay: Duration, action: F) -> Self
    where
        F: Fn() + Send + 'static,
    {
        Task {
            delay,
            action: Box::new(action),
        }
    }
}

Step 2: Create the Scheduler

Next, we will create the scheduler that will manage our tasks. The scheduler will have methods to add tasks and execute them.

use std::collections::VecDeque;

struct Scheduler {
    tasks: VecDeque<Task>,
}

impl Scheduler {
    fn new() -> Self {
        Scheduler {
            tasks: VecDeque::new(),
        }
    }

    fn add_task<F>(&mut self, delay: Duration, action: F)
    where
        F: Fn() + Send + 'static,
    {
        let task = Task::new(delay, action);
        self.tasks.push_back(task);
    }

    async fn run(&mut self) {
        while let Some(task) = self.tasks.pop_front() {
            sleep(task.delay).await;
            (task.action)();
        }
    }
}

Step 3: Implementing the Main Function

Now we will implement the main function to demonstrate how to use the Scheduler. We will create a scheduler, add some tasks, and run it.

#[tokio::main]
async fn main() {
    let mut scheduler = Scheduler::new();

    // Adding tasks with different delays
    scheduler.add_task(Duration::from_secs(1), || {
        println!("Task 1 executed after 1 second.");
    });

    scheduler.add_task(Duration::from_secs(2), || {
        println!("Task 2 executed after 2 seconds.");
    });

    scheduler.add_task(Duration::from_secs(3), || {
        println!("Task 3 executed after 3 seconds.");
    });

    // Running the scheduler
    scheduler.run().await;
}

Step 4: Running the Code

To run the code, save it in a file named main.rs and execute the following command in your terminal:

cargo run

You should see output indicating that tasks are executed after their respective delays:

Task 1 executed after 1 second.
Task 2 executed after 2 seconds.
Task 3 executed after 3 seconds.

Step 5: Enhancements and Best Practices

While the basic task scheduler works, there are several enhancements and best practices to consider:

  1. Error Handling: Implement error handling for task execution, especially if tasks may fail.
  2. Task Prioritization: Allow tasks to have different priorities and execute them based on priority.
  3. Cancellation: Implement a mechanism to cancel scheduled tasks before they are executed.
  4. Task Result Handling: Modify the Task struct to return results from the executed functions.

Here’s a brief comparison of how our simple scheduler can be enhanced:

FeatureCurrent ImplementationEnhanced Implementation
Error HandlingNoneYes
Task PrioritizationNoYes
CancellationNoYes
Result HandlingNoYes

Conclusion

In this tutorial, we built a simple task scheduler using Rust's asynchronous features. We defined a Task structure, created a Scheduler to manage tasks, and demonstrated how to run scheduled tasks with delays. This example serves as a foundation for building more complex scheduling systems in Rust.

Learn more with useful resources: