In an actor system the TcpStream would be local to an actor. You'd say "parse_line" by sending it a message. That message would be processed to-completion before another message could be processed - even if there is yielding internally.
You could also wrap TcpStream with some sort of wrapper that doesn't expose its internal mutability - that would let the type system ensure there isn't shared mutability across tasks I think...
Canceling an async task feels a lot like aborting a partially executed DB transaction without rolling back any changes.
Is there a reason the function in the article couldn't be written to accept a &mut TcpStream, which doesn't let others interact with the same TcpStream while the function is running or awaiting?
I don't think making it &mut TcpStream would solve the problem. It's not that something else is reading between a parse_line() call's read_u32() and read_exact() but that the parse_line() future is going away entirely between those and the read_exact() never happens (or never completes, as it's probably not atomic either). When that happens, it leaves the TcpStream in an unexpected state (halfway through a line) and the next parse_line() is misaligned.
But I do think having one task which owns reading from the stream to completion would solve the problem. If it dies, the whole TcpStream does too. It could talk with other tasks via channels with atomic messages. (Although I've been trying to minimize my use of channels right now because neither the tokio crates nor the futures crates offer an unbuffered/rendezvous channel, and I don't like sticking extra buffers in the middle of everything.)
[1] https://docs.rs/tokio/1.7.0/tokio/io/fn.split.html
[2] https://docs.rs/tokio/1.7.0/tokio/net/struct.TcpStream.html
Yes, and in fact in older versions of tokio reading and writing required &mut, even for things like UdpSocket, which was a bit of a pain when you wanted to write to a socket concurrently as you couldn't just use an Arc.
Oops. I stand corrected.