gpui-async

cnwzhu's avatarfrom cnwzhu

Async patterns and concurrency in GPUI applications. Use when implementing background tasks, async/await operations, task management, or concurrent operations.

0stars🔀0forks📁View on GitHub🕐Updated Jan 11, 2026

When & Why to Use This Skill

This Claude skill provides comprehensive patterns and best practices for managing asynchronous operations and concurrency within the GPUI framework. It enables developers to architect high-performance Rust applications by mastering non-blocking UI threads, background task execution, and robust task lifecycle management.

Use Cases

  • Implementing non-blocking data fetching: Use cx.spawn to handle network requests without freezing the UI thread.
  • Offloading heavy computations: Utilize cx.background_spawn for CPU-intensive tasks like data processing or complex calculations to maintain fluid UI performance.
  • Managing cancellable operations: Implement search-as-you-type or auto-refresh features where previous tasks need to be dropped and cancelled automatically.
  • Safe state synchronization: Correct-pattern implementation for updating UI entities and notifying the context from within asynchronous blocks using WeakEntity and AsyncApp.
  • Error handling in concurrent systems: Establishing robust error propagation and logging patterns for background tasks to prevent silent failures.
namegpui-async
descriptionAsync patterns and concurrency in GPUI applications. Use when implementing background tasks, async/await operations, task management, or concurrent operations.

GPUI Async

This skill covers async patterns and concurrency management in GPUI.

Overview

GPUI provides async primitives for:

  • Foreground tasks: cx.spawn() for UI thread work
  • Background tasks: cx.background_spawn() for CPU-intensive work
  • Task management: Task<R> for cancellation and lifecycle
  • Async contexts: AsyncApp and AsyncWindowContext for async operations

Foreground Tasks

All UI rendering happens on a single foreground thread.

Basic Spawn

use gpui::*;

impl MyView {
    fn start_work(&mut self, cx: &mut Context<Self>) {
        cx.spawn(async move |this, cx| {
            // this: WeakEntity<Self>
            // cx: &mut AsyncApp
            
            // Do async work on UI thread
            this.update(&mut *cx, |view, cx| {
                view.status = "Working...".into();
                cx.notify();
            })?;
            
            Ok(())
        }).detach();
    }
}

Accessing Entity in Async

fn fetch_data(&mut self, cx: &mut Context<Self>) {
    cx.spawn(async move |this, cx| {
        // Simulate async work
       tokio::time::sleep(Duration::from_secs(1)).await;
        
        // Update entity from async context
        this.update(&mut *cx, |view, cx| {
            view.data = "Loaded!".into();
            cx.notify();
        })?;
        
        Ok(())
    }).detach();
}

Background Tasks

Use background_spawn() for CPU-intensive work that shouldn't block UI.

Background Computation

fn compute_heavy(&mut self, cx: &mut Context<Self>) {
    let input = self.input.clone();
    
    cx.spawn(async move |this, cx| {
        // Spawn background task
        let result = cx.background_spawn(async move {
            // Heavy computation on background thread
            expensive_calculation(input)
        }).await;
        
        // Update UI with result
        this.update(&mut *cx, |view, cx| {
            view.result = result;
            cx.notify();
        })?;
        
        Ok(())
    }).detach();
}

Parallel Background Work

fn parallel_work(&mut self, cx: &mut Context<Self>) {
    let items = self.items.clone();
    
    cx.spawn(async move |this, cx| {
        // Process multiple items in parallel
        let tasks: Vec<_> = items.iter().map(|item| {
            let item = item.clone();
            cx.background_spawn(async move {
                process_item(item)
            })
        }).collect();
        
        // Wait for all tasks
        let results = futures::future::join_all(tasks).await;
        
        // Update UI
        this.update(&mut *cx, |view, cx| {
            view.results = results;
            cx.notify();
        })?;
        
        Ok(())
    }).detach();
}

Task Management

Task Type

Task<R> is a future that can be:

  • Awaited: Get the result
  • Detached: Run independently (.detach())
  • Stored: Cancel when dropped

Detaching Tasks

// Task runs independently, won't be cancelled
cx.spawn(async move |this, cx| {
    // Work...
    Ok(())
}).detach();

Storing Tasks

struct MyView {
    current_task: Option<Task<anyhow::Result<()>>>,
}

impl MyView {
    fn start_task(&mut self, cx: &mut Context<Self>) {
        // Cancel previous task by dropping it
        self.current_task = Some(cx.spawn(async move |this, cx| {
            // Task automatically cancelled if dropped
            tokio::time::sleep(Duration::from_secs(5)).await;
            
            this.update(&mut *cx, |view, cx| {
                view.status = "Done!".into();
                cx.notify();
            })?;
            
            Ok(())
        }));
    }
    
    fn cancel_task(&mut self) {
        // Dropping the task cancels it
        self.current_task = None;
    }
}

Task::ready()

Create a task that immediately provides a value:

fn immediate_value(cx: &mut Context<Self>) -> Task<String> {
    Task::ready("Immediate result".to_string())
}

Detach with Error Logging

cx.spawn(async move |this, cx| {
    // Work that might fail
    might_fail().await?;
    Ok(())
}).detach_and_log_err(cx);

Async with Window

Use .update_in() when you need window access in async contexts:

fn async_with_window(&mut self, cx: &mut Context<Self>) {
    cx.spawn(async move |this, cx| {
        tokio::time::sleep(Duration::from_secs(1)).await;
        
        this.update_in(&mut *cx, |view, window, cx| {
            view.count += 1;
            window.dispatch_action(CountChanged.boxed_clone(), cx);
            cx.notify();
        })?;
        
        Ok(())
    }).detach();
}

Error Handling

Propagating Errors

cx.spawn(async move |this, cx| {
    // Use ? to propagate errors
    let data = fetch_data().await?;
    
    this.update(&mut *cx, |view, cx| {
        view.data = data;
        cx.notify();
    })?;
    
    Ok::<_, anyhow::Error>(())
}).detach_and_log_err(cx);

Handling Errors Explicitly

cx.spawn(async move |this, cx| {
    match fetch_data().await {
        Ok(data) => {
            this.update(&mut *cx, |view, cx| {
                view.data = data;
                view.error = None;
                cx.notify();
            })?;
        }
        Err(e) => {
            this.update(&mut *cx, |view, cx| {
                view.error = Some(e.to_string());
                cx.notify();
            })?;
        }
    }
    
    Ok(())
}).detach();

Variable Shadowing in Async

Use variable shadowing to scope clones clearly:

fn async_with_data(&mut self, cx: &mut Context<Self>) {
    let data = self.shared_data.clone();
    
    cx.spawn(async move |this, cx| {
        // Use cloned data
        let result = process(data).await;
        
        this.update(&mut *cx, |view, cx| {
            view.result = result;
            cx.notify();
        })?;
        
        Ok(())
    }).detach();
}

Common Patterns

Loading State

enum LoadingState<T> {
    Idle,
    Loading,
    Loaded(T),
    Error(String),
}

struct DataView {
    state: LoadingState<String>,
}

impl DataView {
    fn load_data(&mut self, cx: &mut Context<Self>) {
        self.state = LoadingState::Loading;
        cx.notify();
        
        cx.spawn(async move |this, cx| {
            match fetch_data().await {
                Ok(data) => {
                    this.update(&mut *cx, |view, cx| {
                        view.state = LoadingState::Loaded(data);
                        cx.notify();
                    })?;
                }
                Err(e) => {
                    this.update(&mut *cx, |view, cx| {
                        view.state = LoadingState::Error(e.to_string());
                        cx.notify();
                    })?;
                }
            }
            
            Ok(())
        }).detach();
    }
}

impl Render for DataView {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        match &self.state {
            LoadingState::Idle => div().child("Click to load"),
            LoadingState::Loading => div().child("Loading..."),
            LoadingState::Loaded(data) => div().child(data.clone()),
            LoadingState::Error(err) => div().child(format!("Error: {}", err)),
        }
    }
}

Cancellable Task

struct Search {
    current_search: Option<Task<anyhow::Result<()>>>,
    results: Vec<String>,
}

impl Search {
    fn search(&mut self, query: String, cx: &mut Context<Self>) {
        // Cancel previous search
        self.current_search = None;
        self.results.clear();
        cx.notify();
        
        self.current_search = Some(cx.spawn(async move |this, cx| {
            let results = cx.background_spawn(async move {
                // Expensive search
                search_items(query)
            }).await;
            
            this.update(&mut *cx, |view, cx| {
                view.results = results;
                cx.notify();
            })?;
            
            Ok(())
        }));
    }
}

Periodic Task

struct AutoRefresh {
    refresh_task: Option<Task<()>>,
}

impl AutoRefresh {
    fn start_auto_refresh(&mut self, cx: &mut Context<Self>) {
        self.refresh_task = Some(cx.spawn(async move |this, cx| {
            loop {
                tokio::time::sleep(Duration::from_secs(30)).await;
                
                if this.update(&mut *cx, |view, cx| {
                    view.refresh_data(cx);
                }).is_err() {
                    break; // Entity was dropped
                }
            }
        }));
    }
    
    fn stop_auto_refresh(&mut self) {
        self.refresh_task = None;
    }
}

Best Practices

  1. Use .detach() for fire-and-forget: When you don't need the result
  2. Store tasks for cancellation: Keep Task in struct field to cancel on drop
  3. Use background_spawn() for CPU work: Don't block UI thread
  4. Always handle errors: Use ? or explicit error handling
  5. Clone before move: Use variable shadowing for clarity
  6. Call cx.notify(): After updating state from async

Common Mistakes

Mistake Problem Solution
Not using &mut *cx Borrow error Dereference with &mut *cx
Forgetting cx.notify() UI doesn't update Call after state changes
Long-running foreground tasks UI freezes Use background_spawn()
Not handling weak entity None Panic Always use ? or check result
Blocking in spawn UI freezes Use background_spawn() for blocking work

Summary

  • Use cx.spawn() for foreground async work
  • Use cx.background_spawn() for CPU-intensive tasks
  • Call .detach() for fire-and-forget tasks
  • Store Task in fields for cancellation
  • Use ? to propagate errors
  • Call cx.notify() after async updates
  • Use variable shadowing for clones

References