gpui-async
Async patterns and concurrency in GPUI applications. Use when implementing background tasks, async/await operations, task management, or concurrent operations.
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.
| name | gpui-async |
|---|---|
| description | Async 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:
AsyncAppandAsyncWindowContextfor 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
- Use
.detach()for fire-and-forget: When you don't need the result - Store tasks for cancellation: Keep
Taskin struct field to cancel on drop - Use
background_spawn()for CPU work: Don't block UI thread - Always handle errors: Use
?or explicit error handling - Clone before move: Use variable shadowing for clarity
- 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
Taskin fields for cancellation - Use
?to propagate errors - Call
cx.notify()after async updates - Use variable shadowing for clones