5

Looking at this code:

public async Task<T> ConsumeAsync()
    {
          await a();
          await b();
          await c();
          await d();
          //..
    }

Let's say that a,b,c,d also have nested async awaits (and so on)

Async/await POV - for each await , there is a state machine being kept.

Question (theoretical):

As each state machine is kept in memory, could this cause big memory consumption?
It might be a vague question to ask, but if there are many states, it seems inevitable not to wonder about the sizes of state machines being kept.

  • Note that if they complete synchronously (which does happen, more than you'd think) there is no allocation - the state machine is a struct and is only boxed when preparing to schedule a continuation. And... either way, it is pretty small. – Marc Gravell Apr 21 at 9:12
  • @MarcGravell they will only be synchronously , if task has already finished by the time await gets there. No ? – Royi Namir Apr 21 at 9:13
  • @Marc Gravell are you sure it's a struct? Here it decompiles to a sealed class that implements IAsyncStateMachine. – Theodor Zoulias Apr 21 at 9:58
  • "for each await , there is a state machine being kept." -- not quite. For each method containing awaits, a state machine is kept. So there's a single state machine for your entire async method, not one for each await statement. – canton7 Apr 21 at 10:09
  • 1
    @TheodorZoulias almost certainly depends on the +optimize flag. Frankly, I'm only ever interested in the optimized build anyway... – Marc Gravell Apr 22 at 10:29
6

Async/await POV - for each await , there is a state machine being kept.

Not true. The compiler generates a state machine for each async method. Locals in the method are lifted into fields on the state machine. The body of the method is (basically) broken into a switch statement, with each case corresponding to part of the method between await statements. An int is used to keep track of which bit of the method has been executed (i.e. which case should be executed next).

Your methods a(), b(), etc, might have their own state machines, or they might not (depending on whether they're marked async or not). Even if they do, in your example only one of those state machines will be instantiated at a time.

SharpLab is a great resource for exploring this stuff. Example.

  • Not true. Each time an await command is executed, a new instance of the state machine class is created. – Theodor Zoulias Apr 21 at 10:29
  • 2
    @TheodorZoulias Please provide some evidence for that claim. The state machine is a struct, and it's boxed on the first await here, then the box is cached for subsequent awaits. – canton7 Apr 21 at 10:30
  • Look at this example. The command await a(); is compiled as awaiter4 = a().GetAwaiter(); Calling method a() causes this command to execute <a>d__1 stateMachine = new <a>d__1(); – Theodor Zoulias Apr 21 at 10:37
  • 1
    @TheodorZoulias <a>d__1 is the state machine for the a() method, not for the ConsumeAsync method. See my 2nd paragraph starting "Your methods a(), b(), etc, might have their own state machines, or they might not", and indeed my 2nd sentence "The compiler generates a state machine for each async method.". I asserted that the state machine for the ConsumeAsync method is only instantiated once. I also said that only one of the state machines for a(), b(), etc would be instantiated at a time. I'm basically repeating my answer back to you in this comment: please re-read my answer. – canton7 Apr 21 at 10:40
  • 2
    1) Each async method gets a single state machine, 2) That state machine is instantiated each time the method is called, 3) Different methods have different state machines. An async method's state machine services the entire method. No method has more than one state machine. – canton7 Apr 22 at 9:22
8

As each state machine is kept in memory , could this cause big memory consumption ?

Very unlikely. Each state machine will occupy a few dozen bytes, at the outside.

So it will only matter when you have very many of them. Nesting isn't really going to cause that, but executing the members of a Task[] might.

But that is not really new or different form any other resource type.

3

There is an additional cost, but it is relatively slim.

Additional costs compared to regular function:

  • A class for the state machine
  • instance of this class
  • one int for the stage of execution
  • AsyncTaskMethodBuilder instance

Additionally, local variables of the function will be transformed into fields of the state machine. This moves some memory from a stack to the heap.

I recommend decompiling some simple async function, to see the generated state machine and have an intuition what to expect.

There are some online tools to do this as well (like sharplab.io) See results of decompilation of a trivial async function

  • Decompiling async function will not expose the amount of memory is taken . – Royi Namir Apr 21 at 10:07
  • It will allow to see what variables are generated for the state machine and have an intuition on the overhead. I agree that you won't have a precise answer in bytes – Lesiak Apr 21 at 10:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.