How long have you been using the syntax: Used preawait!()
model for about three months, then await!()
for a month in tide
, using .await
now for a few days.
What code are you working on: A game server for a small amount of clients, experimental inhouse and completely private environment. Porting stuff from futures = 0.2
to futures-preview=0.3.0-alpha.16
.
What environment: NeoVim, with a slightly outdated set of plugins including RLS. Sometimes updating to the newest versions broke stuff and I’m currently not comfortable nor have the time to experiment which other setup would work.
What happened: There were positives and negatives. Little concerns with the syntax of .await
itself, though using .await
felt smoother than the macro and also chaining; It mostly improved many error cases such as all async I/O now properly returning Result<usize>
although I ignore the count in most cases. This is easily done by chaining .await.map()
as to any Result
. The error types are much smoother due to concise ?
operator instead of chaining into
.
Impractical is that the idiomatic future type is hard to name. Multiple times I had stored it in some intermediate structure previously but can no longer do this neatly due to returning an anonymous type. I don’t want to introduce new, unecessary type parameters. So, I now store an intermediate that is not an actual future and has a function for turning it into one. This gets passed to the coroutines that turn it into a proper future and wait on that. It works. May likely switch to IntoFuture
if it gets included into stable but not necessary currently.
// Previously implemented `Future`. Now doesn't, Pin made it hard to rework.
struct Send { ... }
// Now
impl Send {
// Can't name this Future type directly, although it is unique.
async fn really_send(self) -> Result<usize, Error> { ... }
}
The rework itself was necessary since there are no longer futures for socket.send()
which take ownership of the to-be-sent data. While optimized for async fn
where borrowing is definitely cheaper, it makes it even harder to name specific futures. In a async fn broadcast
function I share allocated bytes data via an Arc<str>
to multiple async fn send
, without any type parameters. This would definitely become a core data structure in synchronous usage but without being able to name it, it is not currently in asynchronous usage. Though the finalized solution now simply sends from borrows of the data without requiring Arc
as a tradeoff, there is not enough available guidance on how one would create such a type manually.
Some more on pinning overall: StreamExt::next
requires the stream to be Unpin
. While necesary, there is little to no help offered. When possible, stack-pinning e.g. this pin_mut macro is the cheapest way to achive this. It is far from obvious that this is possible and nothing indicates it. Requiring a third-party crate to resolve doubts of it being safe is also suboptimal. I’m also afraid that the compiler error messages are not as helpful as they could be. Especially if the type of the future requiring pinning is itself not named anywhere but purely deduced (so basically anytime the value comes from a async fn
) this becomes quickly confusing and is reminiscent of C++ template failures
.
Regarding highlighting—which doesn’t work due to outdated plugin—it doesn’t feel too limiting. The weirdest occurances was when matching an .await
. For other fields matching foo.field
by ref
does not move from foo
, so I accidentally stumbled due to the lifetime of the bound temporary. Nothing major, just takes a bit getting used to. The nicer code pattern would be to introduce a temporary, the argument to match
is likely never better off being an expression. Field syntax is however fairly suggestive here to move it into the match
. It would however help in no way to be prefix await
over .await()
or similar. Due to the difficulties storing async fn
directly without type parameter overheads, I now sometimes (~5%–10% of times) have two operations to the construction of the actual future. Clarity of postfix is better both in this case and the case of mapping or field accessing the result, totalling ~25% of my usage.
In the rework I once blundered by awaiting sequentially instead of using select!
, which meant a loop locked up. This was in one of those match
of an .await
, which I think contributed to my mistake and made it harder to spot. The sending end for a client has both a message queue and a kill switch to kick misbehaving ones without further overhead. However, the kill switch is not always present, so that code now looks similar to:
if let Some(ref mut kill) = self.kill_switch {
select! {
msg = self.queue => deal_with(msg),
killed = kill => match killed { ... },
};
} else {
deal_with(self.queue.await)
}
This is far from optimal. I didn’t find a lot on how to improve this; though I suppose a wrapper around Option<&mut impl Future>
could provide an infinite block in the None
case which would make it possible to select!
in all cases of control flow.
Other background information: Not comfortable with sharing source code, sorry. Other than the blunder, rework was fairly uneventful. Even moving between my own reactor, romio
, mio
(I had my own uds
adaption since the other ones don’t support Linux specific abstract sockets–see section Address format.abstract. I no longer use them, since it has gotten hard to maintain).