在Tokio遇到的生命周期问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
use core::time;
use sysinfo::{ProcessesToUpdate, System};
use tauri::State;
use tokio::time::interval;
use tracing::{debug, info, warn};
use crate::state::AppState;

pub struct Watcher<'a, T>
where
    T: Fn() + Send + 'static,
{
    pub process_name: &'a str,
    pub on_start: T,
    pub on_stop: T,
    pub is_running: bool,
}

impl<'a, T> Watcher<'a, T>
where
    T: Fn() + Send + 'static,
{
    async fn watch_process(&mut self) {
        info!("Starting to watch process: {}", self.process_name);
        let mut interval = interval(time::Duration::from_secs(2));
        let mut system = System::new_all();

        loop {
            interval.tick().await;
            system.refresh_processes(ProcessesToUpdate::All, true);

            let process_is_running = system
                .processes()
                .iter()
                .any(|(_, process)| process.name().eq_ignore_ascii_case(self.process_name));

            if self.is_running != process_is_running {
                self.is_running = process_is_running;

                if self.is_running {
                    (self.on_start)();
                } else {
                    (self.on_stop)();
                }
            }
        }
    }

    pub async fn start(&mut self, state: State<'_, AppState>) {
        let task_guard = state.watch_task.lock().await;

        if task_guard.is_some() {
            warn!("{} already in watching, skipping", self.process_name);
            return;
        }

        let task = tokio::spawn(self.watch_process()); // 这句出现生命周期问题
    }

    pub fn stop() {
// TODO
    }

}

报错具体语句

1
2
3
4
5
let task = tokio::spawn(self.watch_process());
// borrowed data escapes outside of method
// `self` escapes the method body here

// argument requires that `'a` must outlive `'static`

原因分析:

  1. self 的生命周期 ('a) vs. tokio::spawn 的生命周期 ('static):
    • Watcher 结构体定义了一个生命周期参数 'a,并且 process_name 字段的类型是 &'a str。这意味着 process_name 是一个借用的字符串切片,它的生命周期与 Watcher 实例的生命周期 'a 绑定。
    • tokio::spawn(self.watch_process()) 会创建一个新的 Tokio 任务。Tokio 任务通常需要捕获它们所使用的变量,并且这些变量必须具有 'static 生命周期。这是因为 Tokio 任务可以在任何时候被调度,并且它们的生命周期可能比创建它们的函数或方法的生命周期要长。
    • 当尝试将 self.watch_process()(一个方法,它隐式地借用了 self)传递给 tokio::spawn 时,Rust 编译器会发现 self 的生命周期 'a 可能比 tokio::spawn 所期望的 'static 生命周期要短。如果 'a 短于 'static,那么 self(以及它所引用的 process_name)就可能在任务还在运行时失效,导致悬空指针,这是 Rust 极力避免的。
  2. self 逃逸:
    • tokio::spawn 期望捕获的数据是 'static 的。当 self.watch_process() 被调用时,它需要访问 self(即 Watcher 实例)。因为 self 拥有一个非 'static 的生命周期 'a,并且 tokio::spawn 需要 'static,所以 self 的借用“逃逸”了 start 方法的局部作用域。

解决方案

方法一:让 Watcher 结构体本身拥有 'static 生命周期(如果可能)

如果 Watcher 结构体本身不需要借用任何外部数据(除了 'static 的数据),那么你可以尝试让它拥有 'static 生命周期。然而,由于 process_name: &'a str,这直接阻止了 Watcher 拥有 'static 生命周期。

方法二:将 Watcher 的所有权移交给任务(更常见)

最直接的解决方案是让 Watcher 实例的所有权转移到 tokio::spawn 创建的任务中。这意味着 Watcher 实例本身需要被移动到任务中,而不是被借用。

要做到这一点,你需要:

  1. 移除 Watcher 结构体中的生命周期参数 'a
  2. 将 process_name 的类型从 &'a str 改为 String 这样 process_name 就拥有了 'static 生命周期(或者至少是它自己的所有权,可以被移动)。
  3. 将 watch_process 方法标记为 async,并且让它接收 self 的所有权(self: Watcher<'static, T> 或直接 self)。
  4. 在 start 方法中,将 Watcher 的所有权移动到 tokio::spawn