iced icon indicating copy to clipboard operation
iced copied to clipboard

Can't run on a single-threaded executor, as the manual polling of `run_instance` blocks the executor

Open MarcusGrass opened this issue 3 months ago • 0 comments

Is your issue REALLY a bug?

  • [x] My issue is indeed a bug!
  • [x] I am not crazy! I will not fill out this form just to ask a question or request a feature. Pinky promise.

Is there an existing issue for this?

  • [x] I have searched the existing issues.

Is this issue related to iced?

  • [x] My hardware is compatible and my graphics drivers are up-to-date.

What happened?

I tread running a single-threaded executor:

fn main() {
    iced::application(App::default, App::update, App::view)
        .executor::<LocalTokioExecutor>()
        .title("my app")
        .decorations(true)
        .theme(App::theme)
        .run()
        .unwrap();
}

struct LocalTokioExecutor {
    runtime: tokio::runtime::Runtime,
}

impl iced::executor::Executor for LocalTokioExecutor {
    #[inline]
    fn new() -> Result<Self, Error>
    where
        Self: Sized,
    {
        tracing::info!("Creating local tokio runtime");
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            //.build_local(tokio::runtime::LocalOptions::default())
            .build()
            .map(|runtime| Self { runtime });
        tracing::info!("Created local tokio runtime");
        rt
    }

    #[inline]
    fn spawn(&self, future: impl Future<Output = ()> + iced_futures::MaybeSend + 'static) {
        eprintln!("Trigger spawn");
        self.runtime.spawn(async move {
            eprintln!("Spawning future");
            future.await;
            eprintln!("Future finished");
        });
    }

    #[inline]
    fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
        eprintln!("Blocking on future");
        let res = self.runtime.block_on(future);
        eprintln!("Future finished");
        res
    }

    #[inline]
    fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
        eprintln!("Entering runtime");
        let _g = self.runtime.enter();
        let res = f();
        eprintln!("Exiting runtime");
        res
    }
}

No window pops up.

After looking at the code, I see that spawned futures are never picked up and executed. After following it further, it's a bit of a systemic problem. The executor (if running CurrentThread) is the thread that starts and drives the event-loop handler. That gets stuck perpetually polling run_instance and doing nothing else, no background tasks are making any progress.

I think this would need a refactor of the main event handler to solve, but I'll try to fiddle around with it a bit if I find the time.

F.E. this solves it:


struct LocalTokioExecutor {
    runtime: tokio::runtime::Handle,
}

impl iced::executor::Executor for LocalTokioExecutor {
    #[inline]
    fn new() -> Result<Self, Error>
    where
        Self: Sized,
    {
        tracing::info!("Creating local tokio runtime");
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()?;
        let handle = rt.handle().clone();
        std::thread::spawn(move || rt.block_on(async { tokio::signal::ctrl_c().await }));
        tracing::info!("Created local tokio runtime");
        Ok(Self { runtime: handle })
    }
    ...

Because then a thead can drive what's spawned onto the runtime. That does make it non single-threaded though. So I wouldn't call that a proper workaround.

What is the expected behavior?

Everything works as expected from a multi-threaded executor

Version

master

Operating System

Linux

Do you have any log output?


MarcusGrass avatar Sep 10 '25 19:09 MarcusGrass