Novel, Safe Uses for sync::Weak?

I had a thought about trying to optimize long-lived values that exist in an Arc<T> and wanted to see if these novel uses make sense with what sync::Weak<T> is designed to do.

The docs give an example of using Weak<T> for graphs for a node that needs to reference its parent.

I had an idea for more uses for Weak<T> and wanted to understand if it is sufficient/appropriate.

Example: Web Server

Here is the example and documentation that axum provides for sharing state:

use axum::{
   extract::State,
   routing::get,
   Router,
};
use std::sync::Arc;

struct AppState {
   // ...
}

let shared_state = Arc::new(AppState { /* ... */ });

let app = Router::new()
   .route("/", get(handler))
   .with_state(shared_state);

async fn handler(
    State(state): State<Arc<AppState>>,
) {
    // ...
}

State is cloned for every request. Wrapping your state in Arc makes those clones cheap. If all fields are already cheap to clone (for example, each field is itself an Arc or a copy type), you can #[derive(Clone)] directly on the struct instead.

For the sake of my question, I'm going to assume that the following invariant is upheld, that no handler execution shall outlive the axum::Router itself. I am aware that this might be impossible because I could tokio::spawn or spawn a new thread within a handler to escape this, but I'm going to pretend it is true. I'm also not particularly concerned here in my example with the particulars of axum, it's just an example.

If such a guarantee were able to be made, would using Weak<T> references be safe and would remove the atomic increment/decrement on the Arc for every request? I'm aware that in this example we are I/O bound and atomic contention on the CPU is probably not at all affecting things, but I still want to understand if this is a safe way to eliminate this sort of +1/-1 on every handler execution.

Not answering the original question (I'm honestly not sure) but I'm curious - for the same problem, it seems like Box::leak would solve it the same way. But trade "hope this isn't dropped or else UB?" for "hope this isn't constructed repeatedly or else constant memory increase until OOM"

IIRC, Weak still has to increment a different counter in the underlying allocation, so that it doesn't get deallocated while Weaks to it still exist. So you have the exact same situation with increments/decrements, except the underlying data might get dropped (but not deallocated).

Using Weak is counterproductive: As @Kyllingene pointed out, the Arc implementation uses two counters, one for the strong references and one for the weak references.

So cloning a Weak increments the weak counter, and to access the data, you need to get a strong reference, which results in much more work to be done (the strong counter could not be simple increased, because maybe it could go zero by another thread just in the moment you increment it, and the other thread will drop the data).

But when you have already a strong reference, this is literally only an increment, because the data could not be dropped in the meantime.

So, cloning a strong reference is cheap, literally. But there is another argument:

How many requests per second are you expecting to serve? A thousand, tenths of thousands or hundred thousands? How much CPU time is spent for one increment and one decrement, relatively compared to the complete processing per request? 0.001% or even less?

Long story short: For your proposed example: It's worth nothing.

if the resource should always stay alive, why bother with reference counting? just use a shared reference if you have a scoped api, or put it in a static variable (or Box::leak()).

after all, one of the premise of Weak is to allow the resource to be dropped when there's no "strong owners".

also unrelated to the question, but if the contention in the program is so bad that the atomic reference counters become a performance concern, your problem is probably deeper in the architectural design. for example, you may just stop using a globally shared resource and use thread local replicas instead. or use purely message passing and ditch the shared resources altogether (just like Erlang).