How do people find deadlocks in their Tokio code? Are there any static tools/lints? I stupidly just wrote:
// value calculation is idempotent, so fine if another thread nips in and populates the cache before us
match cache.lock().await.get(k) {
Some(v) => v.clone(),
None => {
let result = calculate_value(k);
cache.lock().await.insert(k,v)
}
}
It obviously deadlocks on the second lock because locks (to my surprise!) aren't reentrant.
But surely a lint or tool could find this sort of thing?
I don't use async locks. Per here: "an asynchronous mutex is more expensive than an ordinary mutex, and it is typically better to use one of the two other approaches".
I would be interested to know why you would be using them, as I cannot really imagine a situation where it would be a good idea. So just avoid would be my recommendation.
Thanks @geebee22. Elsewhere I need to hold them across awaits, which require an async mutex, but the above code would still lock with sync mutex AFAIK.
You can tell that the locks aren't reentrant because they give mutable access to the contents. A reentrant lock can only give immutable access (but the contents may be Send + !Sync so they can contain a Cell or RefCell)
It's probably worth pointing out that deadlock detection of async locks is fundamentally impossible because locking the mutex in both branches of a tokio::join! is indistinguishable from your code, but if they are in separate branches of a tokio::join!, then its not necessarily a deadlock.
thanks @alice , @kornel, @SkiFire13 for your comments :-). Tokio console looks fantastic, but I'm still struggling to figure out how to combine tracing with the rest of the log eco system (e.g. log4rs).