Async-await experience reports

Could you explain more about this? Or give examples?

One example: I have a method which allows to send messages, but internally needs to perform rate-limiting in order to guarantee that the given resources are never exhausted. In order to do this, I’m using an (async) semaphore. Now the tricky thing was to release the semaphore reliably when the remaining async operation failed, and keep the permit and release it later if the sending step completes.

Pseudecode:

async fn send_message(&self, content: &str) -> Result<MessageId, Error> {
    self.rate_limiter.acquire(1).await; // Acquire 1 permit of async semaphore
    
    let serialized_message = serializer.serialize(content);

    match writer.enqueue_message(serialized_message).await {
        Ok(id) => {
            // Sending the message was fully successful and the ownership had
            // had been passed. The permit now will get released when a response
            // is received.
            Ok(id)
        },
        Err(e) => {
            // Sending the message failed. Since the permit hasn't been used,
            // release it immediately.
            self.rate_limiter.release(1);
            Err(e)
        }
    }
}

This code has an hard to find bug: If the caller aborts the task (by dropping the returned Future) after self.rate_limiter.acquire(1).await; and before match writer.enqueue_message(serialized_message).await had been fully executed then the semaphore permit will have been permanently lost.

I worked around this particular issue in meantime by letting the Semaphore Future resolve to a RAII type which automatically releases the permit when not otherwise instructed, and disable/mem::forget this method in the success case. That’s an even better solution for this case, but one will only find it if one is aware that this can happen.

Another occurrence: I have a subtask, and at the end of the task I want to signal to some other component that the subtask finished. The first solution was:

async fn task(&self) {
    // other code including .await statements

    other_component.notify_this_task_has_finished();
}

This also doesn’t work, since the method is not guaranteed to be called (but I rely on it). In order to work around this, I then built a workaround defer()/ScopeGuard mechanism:

async fn task(&self) {
    let _guard = ScopeGuard::new(|| {
        other_component.notify_this_task_has_finished();
    });
    // other code including .await statements
}
15 Likes

This is really helpful, thanks. One of the things I’m thinking about is how to prioritize and communicate the next steps beyond async-await, and I think that getting feedback on which problems are coming up is key.

9 Likes

While I don’t have an opportunity to write code that uses .await myself, I have seen one on Twitter. Here’s my impression if a code reader’s experience counts.

To sum it up, I had quite a hard time finding where .await is used, mainly due to .await blending in a forest of method calls.

3 Likes

Thanks for explaining! This does indeed seem like a major footgun that people need to be aware of.

Wouldn’t it be better to have the RAII type always release the permit (even in the Ok case)? Then it would look like this:

async fn send_message(&self, content: &str) -> Result<MessageId, Error> {
    let _lock = self.rate_limiter.acquire(1).await; // Acquire 1 permit of async semaphore
    
    let serialized_message = serializer.serialize(content);

    writer.enqueue_message(serialized_message).await
}

Maybe there’s something in your architecture that prevents that, but that’s what I would do.

This is really interesting. It seems to be an issue with any resumable function, so I assume the same issues will happen with generators as well.

People have a strong intuition that functions run to completion, and async violates that intuition.

Languages like JavaScript don’t have that problem because they don’t have cancellable Promises (so async always runs to completion).

1 Like

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 :crazy_face: .

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).

7 Likes

Yes, it’s a little bit more complicated because sometimes the permit needs to be released outside of the scope of this function. This one is only for sending a message, but since I need to limit the amount of transactions in flight I need to wait until the server responds before releasing it.
But that’s an implementation detail. And as long as one thinks about it one will come to a suitable solution. My concern here was more that the necessity to handle early cancellation/termination scenarios might not obvious to everyone, and that I might need to expect a certain amount of bugs due to this.

I think for async/await Rust might be the only language with this property. JavaScript, C# and Kotlin are all run-to-completion (expect for the fact that methods might obviously throw exceptions, but I consider this a slightly different story). C++ coroutines are very flexible and configurable. As far as I understood the semantics (e.g. through this response from Lewis Baker) the ecosystem is planning on using the ability to cancel during yield points only for generator-like semantics, which may not resume after a yield (makes a lot of sense). However the coroutines implementations for async tasks are run-to-completion and can only be cooperatively canceled (e.g. through a CancellationToken.

1 Like

Right, I think the situation is different, because in those languages you have try / finally to handle cleanup, which is very intuitive.

The closest thing Rust has to that is specialized RAII types (which you ended up using). So they’re not quite the same thing.

The only language I know of with cancellation mechanics (similar to Rust) is Conductance, and they have a try / retract syntax (similar to try / finally) to intuitively handle cancellation.

Majority of the community is focused on trivialities like where does await go, but this is going to be the fundamental “unfamiliar”, “easy to get wrong”, “hard to debug” and “super frustrating” aspect of async/await in Rust.

Having said that, this code was not panic-safe in the first place, and it was brittle anyway: in case of refactoring etc. it was easy to introduce an early return and make it buggy. RAII version is more idiomatic and just way better.

It is however something to think about - maybe there’s some smart way to add some lints, warning, anything that would at least educate users. At very least we will have to be really vocal that async functions can stop executing at any point. I guess it’s similar to pitfalls created by iterators being lazy.

14 Likes

Hi there, here is my 50 cents.

How long have you been using the syntax: more than 6 month

What code are you working on: Commercial project - scraping data from different sources(providers).

What environment: vscode (using rls and rust-analyzer both eating almost half of my 16G memory) intellij-idea(no debugging:( )

What happened: I have tried new .await syntax in several places. I don’t feel significant usability increase of that approach, because I don’t has a lot of futures which outputs futures to leverage full power of the postfix syntax (interesting what kind of project may have a lot of futures outputting futures?), in most of the times .map or .and_then is enough IMHO. Here some very typical part of the project:

       let urls_len = await!(self.listing_urls_repo.count(providers.clone(), skip))?;
       let stream = await!(self.listing_urls_repo.list(providers, skip))?;

       let stream = stream
           .map(move |res| {
               let mut sender = sender.clone();

               async move {
                   let res: Result<_, Error> = try {
                       let url = res?;
                       let kind = url.provider.clone().try_into().unwrap();
                       let id = url.provider_id;
                       
                       let new_listing = await!(self.providers.get_listing(kind, id.clone()))
                           .map_err(|err| ErrorKind::FetchFail(format!("provider: {}; id: {}; err: {}", kind, &id, err)))?;

                       let url_update = UpdateListingUrl {
                           id: url.id,
                           status: 1
                       };

                       await!(sender.send((new_listing, url_update))).unwrap();
                   };

                   res       
               }
           })
       .buffer_unordered(100);

       pin_mut!(stream);
       while let Some(_item) = await!(stream.next()) {
           // increment progress bar
       }

And what I would be happy rust have instead of .await:

  1. pattern matching ? - it is most desirable, here is example:
let task1 = async move { ... }
let task2 = async move { ... }

let (val1?, val2?) = await!(futures::join(task1, task2));
// now I has something like:
// let (res1, res2) = await!(futures::join(task1, task2));
// let (val1, val2) = (res1?, res2?);
// in most cases just
// let _ = (res1?, res2?);
  1. more ergonomics with try + async block. One more annoying thing is async move and error type elision:
let task = async move {
   try { // error type not elided :(
       let a = await!(x)?;
       //    ...
   }
}

and I have to write like:

let task = async move {
   let err: Result<_, Error> = try { // error type not elided :(
       let a = await!(x)?;
       //    ...
   };

   err
}

Other background information: initially was based on futures 0.1 with tokio’s compatibility to futures 0.3

now (few month ago) I have got rid of tokio, used futures 0.3’s block_on and withoutboats’s juliex for spawn and now I have futures 0.3 with compat 0.1 in some cases (like when dealing with tokio-postgres)

find -name *.rs | xargs wc -l - 11646

find -name *.rs | xargs rg await! | wc -l - 311

find -name *.rs | xargs rg "async fn" | wc -l - 132

2 Likes

How long have you been using the .await syntax?

Couple of days

What code are you working on? How many lines of code? How complex? How old is the codebase? Can you post a link to it?

Toy programs like a JSONRPC server and a client to consume an existing JSONRPC API. The programs are simple, about 100 LOC. The code is based mainly on the examples from https://github.com/rustasync/runtime.

What environment are you editing the code in? Do you have .await syntax highlighting? (The emacs mode, for example, recently landed a PR to highlight the keyword.)

I used CLion with Rust plugin but was editing the code as a .txt file without syntax highlighting. I wanted to test the hypothesis that the postfix await is harder to spot.

Summarize what happened, both the good and the bad.

The biggest issue was that it was harder to see await without syntax highlighting (in contrast to Python’s prefix await is immediately obvious).

To improve readability I invented my own postfix syntax where .await was replaced with .:await.

To run the program, I used something like:

sed 's/\.:await/\.await/g' main.rs.txt > main.rs && cargo run

For example, I wrote the following:

while let Some(stream) = incoming.next().:await {
    //...
}

and

if let Ok(n) = buf_stream.read_line(&mut line).:await {
    //...
}

I found that .: slightly improved the readability but I would still need to look for it. Also .:await was a bit slower to write compared to .await.

In docs where the await was highlighted, the keyword was immediately spottable.

Background Used Python’s await for some years, for Rust I prefer the postfix await.

6 Likes

Minor experience report:

How long have you used .await

None with a compiler, in examples from around midway through the GitHub thread.

In what environment

Plaintext markdown editing primarily on Discource-based sites.

Experience

I wrote this post about the theoretical potential of (ab)using Future and async fn to write game-style coroutine cooperative multi-frame multitasking.

In the specific case of creating the “empty” future whose only point is to register the wake up later and immediately awaiting it, postfix await feels backwards. This may be able to be mitigated by better naming than my example next_frame().await.

This same feeling of being reversed didn’t occur with examples where actual work was done in the Future; it’s this specific “suspend only” future that feels out of place for the “pipeline operation” version of await.

(Abusing futures to emulate generators abused to get cooperative semicoroutines leads to awkward syntax, who would have thunk.)

2 Likes

Interesting. It doesn’t seem that inconsistent, though… Imagine if you were doing this using threads instead of async. Then next_frame() would return a Condvar or similar type, and the syntax would be something like next_frame().wait(), which is the same order as .await.

(In reality it would be a bit more complicated because condvars need an associated mutex and can have spurious wakeups; a better fit would be an “event” type, but Rust’s standard library doesn’t have one of those. I guess you could use a mpsc::Receiver<()>…)

3 Likes

How long have you been using the .await syntax?

About a week.

What code are you working on?

udoprog/setmod, work being done in a separate branch (async-await) but the port now runs which was my initial goal.

Right now it has 122 uses of .await. Project is 17k LOC.

What environment are you editing the code in?

VS Code, no syntax highlighting for .await.

Summarize what happened

The .await postfix syntax had almost no impact in terms of weirdness for me. Interactions with it felt very intuitive after a short period of time (a couple of hours). I was initially in the await!(...) macro camp but this is experience has taught me how nice .await is. The biggest win for me is that there’s no need to break up existing code when you introduce new async functions in a call chain which leads to cleaner patches.

The biggest issue is that async blocks and functions suffer from rather scary diagnostics when something goes wrong. I faced lifetime issues from forgetting to move things appropriately into the block or function. This is the patch that fixes it in my project.

Futures not being Send due to something !Send leaking across awaits is another thing that has unhelpful diagnostics. This is the patch that fixes it in my project.

How to work with 0.1 <-> 0.3 compatibility needs more examples and should be visible on docs.rs. For reference, this is available on the privately hosted documentation.

Background information

Rust user for 3 years. I do a programming stream and users on stream didn’t express confusion with the syntax once explained.

15 Likes

How long have you been using the .await syntax?

On and off playing with it since it became available in nightly, only one real but tiny project.

What code are you working on? How many lines of code? How complex? How old is the codebase?

A small service used in house not facing the internet for some UDP packet processing. Currently only 2100 lines of code. I previously wrote it using “stable” future syntax, now converted to .await

Otherwise just experimenting and for personal stuff.

What environment are you editing the code in? Do you have .await syntax highlighting?

Vim, no highlighting yet, maybe it’s already available, I didn’t bother yet to search.

Summarize what happened, both the good and the bad.

I’m really pleasantly surprised! The code using the .await syntax feels more easy to read and understand, even without syntax highlighting. I added a blank line here and there to catch the attention, but I expect that with highlighting the await points will be quick to spot in any way.

I feel that I very quickly got accustomed to await being a new keyword, and the .await syntax feels all fine, I never stumbled upon a thought like “oh wait, this looks like a field, but is it really?”

Guessing from how quickly I got accustomed with .await I strongly feel that it is the way to go!

Compiler error messages can improve, as always. I can’t say whether the error messages were more or less helpful compared to current “stable-style” futures.

I really enjoy using .await

18 Likes

How long have you been using the .await syntax?

A couple of days ago I converted two of my crates from await!() to .await.

What code are you working on?

webdav-handler and webdav-server

What environment are you editing the code in?

vim

How the new syntax feels over time.

Before I used it I thought that suffix dot-await was a brilliant idea. However it only really shines when chaining. In other places it feels kind of weird.

For example:

-    match await!(dav_if_match(req, fs, ls, path)) {
+    match dav_if_match(req, fs, ls, path).await {

Here, visually, I’d prefer

match await dav_if_match(req, fs, ls, path) {

Similar for while let Some(value) = await stream.next() etc

I will probably get used to it.

15 Likes
1 Like

I noticed that this thread became quiet about 25 days ago. There has been a single report in the last 14 days. Until that point, the reports were from people who had used the new syntax for “about a week” or “a few days”.

Coupled with the post about testing needed for async/await, it sounds to me like the feature should bake for a few releases — enough time so that people can really try it out in practice.

“Bake for a few releases” could be made more concrete by finding one or more big projects that currently use await!(), and then wait until they’ve updated their code to use .await. If they don’t see the claimed benefits of chaining .awaits, well then perhaps the syntax isn’t actually doing what people hoped it would do.

5 Likes

How long have you been using the .await syntax?

This is my first time doing anything more than a single line of code with async/await.

What code are you working on? How many lines of code? How complex? How old is the codebase? Can you post a link to it?

It’s really short, 36 lines that I wrote in the last couple of hours.

What environment are you editing the code in?

VS Code with the Rust (rls) extension

Summarize what happened, both the good and the bad.

Sorry, this isn’t a summary, kind of the opposite in fact - I just made notes while I was going through the process. Hopefully it’s useful feedback. Feel free to ask me questions.

Click to expand
  • I’m using the runtime crate. The name is a little odd, but it’s a nice crate and easy to get started with. I particularly like the spawn(async {...}) pattern. Since it looks a lot like spawning a thread, I decided wanted to make a producer/consumer example using a channel.

  • There’s no channels in the runtime crate, they are in the futures crate. Ok, so I used futures::channel::mpsc::channel to create a sender and receiver for a bounded channel. How do I send something to the channel? There’s methods called try_send and start_send. Neither of them look right. I’m expecting something that returns a future so I can .await it, but neither of them does. Maybe there’s something in the Sink trait that does this… OK, not in Sink but in SinkExt, there is a send method. Except, it says that it flushes the sink - does that mean it will wait until the buffer is empty? Oh well, let’s just try it. use futures::SinkExt. Sigh… this really should be an inherent method on Sender, or at least the documentation should show you how to actually use it. Here’s the code, excluding imports:

    #[runtime::main]
    async fn main() {
        let (mut sender, mut receiver) = channel::<i32>(3);
    
        spawn(async move {
            sender.send(42).await.expect("send failed");
        });
    }
    

    I tried using .await? on the result of the call to send, but it doesn’t work because we’re in an async block, not a closure, and ? only works on functions. It will be nice if we get async try blocks some day. For now, I’ll just let it panic if there’s an error.

  • In the producer, I want to wait a random amount of time and then produce a random number. So I add let rng = thread_rng(); and await a runtime::time::Delay with a random number of milliseconds, then produce a value. Here’s the body of async fn main so far:

    let (mut sender, mut receiver) = channel::<i32>(3);
    
    spawn(async move {
        let rng = thread_rng();
    
        loop {
            let wait_time = rng.gen_range(0, 3000);
            let wait_time = Duration::from_millis(wait_time);
            Delay::new(wait_time).await;
    
            let value = rng.gen();
            sender.send(value).await.expect("send failed");
        }
    });
    

    Uh oh, this produces an error message on the call to spawn:

    error[E0277]: `*mut rand::rngs::adapter::reseeding::ReseedingRng<rand_hc::hc128::Hc128Core, rand::rngs::entropy::EntropyRng>` cannot be sent between threads safely
    

    Well that’s not the best error message, but someone will probably make it better at some point. It’s saying that the async block is not Send because rng is the thread-local RNG. But I’m calling thread_rng() from within the async block, and I thought async blocks don’t start executing until they are polled, so I’m not creating it in this thread and then passing it to that thread, right? I add futures::ready(()).await; to the top of the async block just to make sure. Still the same error.

  • eventually I realize that these are tasks, not threads, and an async block has multiple states where it’s frozen due to an await expression. If at any of the await points there’s a value on the “stack” that isn’t Send, then that means the async block’s type has a field with that type, and therefore the async block isn’t Send. When I realize this, I think that is really unfortunate, since the value being passed to spawn really is Send, and once it starts executing it will be on the same thread the whole time and it won’t matter that it’s doing thread local stuff. But again I remember that these aren’t threads, they’re tasks, and because of this restriction they can be safely moved between threads, which is pretty cool. And it’s probably not that big a deal. In this case I can call thread_rng() directly when I need to generate a random number, instead of assigning it to a local variable. I make that change, and woo-hoo! It compiles! Here’s the code so far:

    let (mut sender, mut receiver) = channel::<i32>(3);
    
    spawn(async move {
        loop {
            let wait_time = thread_rng().gen_range(0, 3000);
            let wait_time = Duration::from_millis(wait_time);
            Delay::new(wait_time).await;
    
            let value = thread_rng().gen();
            sender.send(value).await.expect("send failed");
        }
    });
    
  • I’m getting a couple warnings: one is that receiver is unused and doesn’t need to be mutable, but we’ll get to that. The other is that spawn returns a JoinHandle that must be used. Does it though? I thought that spawn started executing the future. This seems like a false positive — sure, an async block by itself won’t do anything until you .await it or spawn it, but the future that is returned by spawn is probably a special case because it is running. Maybe adding a #[must_use(false)] attribute to the JoinHandle type could fix this. Then again, what if the spawned future’s output type is a Result? There’s probably an issue on GitHub somewhere where they’re discussing exactly this. Anyway, I’ll just save the join handle to a local variable and .await it later:

    let (mut sender, mut receiver) = channel::<i32>(3);
    
    let join_handle = spawn(async move {
        loop {
            let wait_time = thread_rng().gen_range(0, 3000);
            let wait_time = Duration::from_millis(wait_time);
            Delay::new(wait_time).await;
    
            let value = thread_rng().gen();
            sender.send(value).await.expect("send failed");
        }
    });
    
    join_handle.await;
    
  • OK, let’s actually use the receiver and print out the values. I added this code above the join_handle.await:

    receiver.for_each(async move |x| {
        println!("got a number: {}", x);
    });
    

    The move keyword is necessary because async non-move closures apparently aren’t available yet. I was surprised that I didn’t need to use try_for_each - since send on the sender returns a result, I would have thought that the receiver’s next method would do the same.

    This code is nice and neat, but I’m getting an ICE about broken MIR, with the #[runtime::main] macro highlighted. Oh, there’s also a warning that the for_each call returns a future and I’m not using it. The compiler’s actually right in this case. I add .await to the end of the for_each call. No errors, and the ICE is mysteriously gone. Woo hoo! Let’s cargo run this thing:

       Compiling async-stuff v0.1.0 (/Users/mikeyhew/Desktop/async-stuff)
        Finished dev [unoptimized + debuginfo] target(s) in 1.47s
         Running `target/debug/async-stuff`
    got a number: 1045570414
    got a number: -1726706442
    got a number: 413019516
    ^C
    

    It works! That’s it for today, I’m happy with this progress. Here’s the final code:

    #![feature(async_await)]
    
    use {
        runtime::{spawn, time::{Delay}},
        futures::{
            SinkExt, StreamExt,
            channel::mpsc::channel,
        },
        std::{
            time::Duration,
        },
        rand::{thread_rng, Rng},
    };
    
    #[runtime::main]
    async fn main() {
        let (mut sender, receiver) = channel::<i32>(3);
    
        let producer = spawn(async move {
            loop {
                let wait_time = thread_rng().gen_range(0, 3000);
                let wait_time = Duration::from_millis(wait_time);
                Delay::new(wait_time).await;
    
                let value = thread_rng().gen();
                sender.send(value).await.expect("send failed");
            }
        });
    
        receiver.for_each(async move |x| {
            println!("got a number: {}", x);
        }).await;
    
        producer.await;
    }
    
  • Regarding async/await syntax: I initially didn’t like the idea of .await syntax because it was just weird, but I’ve gotten past that initial weirdness. It turns out .await is great in expressions. Even if you’re not calling a method directly on the result, it’s nice to be able to tack on .await or .await? in response to a compiler error message. It’s a little weird as a statement though, when you just want to wait for something to finish before you proceed. For example:

    Delay::new(Duration::from_millis(500)).await;
    

    Syntax highlighting will probably help with that though.

7 Likes

That’s not the case, it can be polled from an arbitrary different thread every time (and in fact, the default runtime-native does exactly that).

2 Likes