Remix.run Logo
cousin_it 5 hours ago

Looks like the code of foo() says "take a lock then sleep for 10 millis", but actually it can take the lock and then sleep forever, depending on how it's polled. Well! This seems like a bug with the async abstraction in Rust then. Or if you don't like "bug", then "disagreement with intuition that will cause bugs forever". Goroutines in Go don't have this problem: if a goroutine says it'll take a lock and sleep for 10 millis, then that's what it'll do. The reason is because Go concurrency is preemptive, while async in Rust is cooperative.

So maybe the lesson I'd take is that if you're programming with locks (or other synchronization primitives), your concurrency model has to be preemptive. Cooperative concurrency + locks = invitation to very subtle bugs.

rcxdude 5 hours ago | parent [-]

I don't think it's preemptive vs cooperative that matters. What Rust's abstraction allows is for a function to act like a mini-executor itself, polling multiple other futures itself instead of delegating it to the runtime. That allows them to contain subtle issues like stopping polling a future without cancelling it, which is, yeah, dangerous if one of those futures can block other futures from running (another way you could come at this is to say that maybe holding locks across async points should be avoided).

cousin_it 4 hours ago | parent [-]

> holding locks across async points should be avoided

Wait, what would be the point of using locks then? It seems to me there's no point taking a lock if you're gonna release it without calling any awaits, because nothing can interfere anyway. Or do you mean cases where you have both cooperative and preemptive concurrency in the same program?

dmgl 2 hours ago | parent [-]

> It seems to me there's no point taking a lock if you're gonna release it without calling any awaits, because nothing can interfere anyway.

This is probably true only in single threaded executor. Other threads often exist.