1 State Machine async Task Foo Async BODY
1. State Machine async Task Foo. Async () { BODY } 2. struct Async. Task. Method. Builder struct Foo. Async_State. Machine : IAsync. State. Machine { //(1, 2, 3) private int _state; public Async. Task. Method. Builder _builder; private void Move. Next () { try { switch (_state ) { TRANSFORMED_BODY } catch (Exception ex) { _builder. Set. Exception(ex); return; } _builder. Set. Result(); } Task Foo. Async () { var sm = new Foo. Async_State. Machine (); sm. _state = -1 ; . . . copy params & this if needed into S. M. sm. _builder = Async. Task. Method. Builder. Create(); sm. _builder. Start( ref sm); return sm. _builder. Task; } private void Set. State. Machine( IAsync. State. Machine sm) { _builder. Set. State. Machine(sm); } } switch (_state) { case 0: goto AFTERAWAIT 0; case -1: // fallthrough } Console. Write. Line ("a"); Task. Awaiter tmp = Task. Delay(100). Get. Awaiter(); //(4) if (!tmp. Is. Completed ) { //(5) _state = 0; _awaiter = tmp; _builder. Await. On. Completed( ref tmp, ref this); //(8) return; AFTERAWAIT 0 : //(9) tmp = (Task. Awaiter )_awaiter; _awaiter = default( Task. Awaiter ); } tmp. Get. Result (); //(6) tmp = default(Task. Awaiter ); //(7) Console. Write. Line ("b"); 6. Lifted local variables int x = 10; await t 1; int z = 10; { int y = 15; Console. Write(x + y + z); } public void Start<TSM>( ref TSM sm) where TSM : IAsync. State. Machine { //(2) Thread t = Thread. Current. Thread; Execution. Context. Switcher ecs = default(Execution. Context. Switcher ); Runtime. Helpers. Prepare. Constrained. Regions(); try { Execution. Context. Establish. Copy. On. Write. Scope(t, false, ref ecs); sm. Move. Next(); } finally { ecs. Undo(current. Thread); } } } internal struct Void. Task. Result { } 4. Await. On. Completed() 3. Awaiter Pattern Console. Write. Line ("a"); await Task. Delay(100); Console. Write. Line ("b"); public struct System. Runtime. Compiler. Services. Async. Task. Method. Builder <T> { internal IAsync. State. Machine m_sm; class Async. Method. Builder <T>: public void Await. On. Completed<TA, TSM>( ref TA a, ref TSM sm) where TA : INotify. Completion where TSM : IAsync. State. Machine { if (m_sm == null) { var ignored = this. Task; // allocate a reference m_sm = ( IAsync. State. Machine )sm; // box on first await m_sm. Set. State. Machine(m_sm); // tie up lose ends } Action cont; var ctx = Execution. Context. Fast. Capture(); if (ctx == Execution. Context. Pre. Allocated. Default ) { cont = m_default. Context. Action; if (cont == null) cont = new Action(new Move. Next. Runner(ctx, this. m_sm). Run); } else { cont = new Move. Next. Runner(ctx, m_sm). Run; } a. On. Completed(cont); } class Foo. Async_State. Machine : private int _x, _z; Move. Next : this. _x = 10; await t 1; this. _z = 10; { int y = 15; Console. Write(this. _x + y + this. _x); } public void Await. Unsafe. On. Completed<TA , TSM>(ref TA a, ref TSM sm) //(11) where TA : ICritical. Notify. Completion where TSM : IAsync. State. Machine { . . . exactly as above a. Unsafe. On. Completed(cont ); } 5. struct Task. Awaiter class Task<T> { Task. Awaiter <T> Get. Awaiter () { return new Task. Awaiter <T>(this); } } struct Task. Awaiter <T> : INotify. Completion { private var m_delegates = new Concurrent. Queue <Action>(); public bool Is. Completed { get; } public T Get. Results () { return. . . ; } public void On. Completed( Action cont) { //(12) var sc = Synchronization. Context. Current; m_delegates. Enqueue( delegate { sc. Post(_ => cont(), null); } } struct Task. Awaiter <T> : ICritical. Notify. Completion { private var m_delegates = new Concurrent. Queue <Action>(); public bool Is. Completed { get; } public T Get. Results () { return. . . ; } public void On. Completed( Action cont) { //(10) var ec = Execution. Context. Capture(); var sc = Synchronization. Context. Current; m_delegates. Enqueue( delegate { Execution. Context. Run(ec, delegate { sc. Post(_ => cont(), null); }, null); } [Security. Critical ] //(11) public void Unsafe. On. Completed( Action cont) { var sc = Synchronization. Context. Current; m_delegates. Enqueue( delegate { sc. Post(_ => cont(), null); } } 7. Nested “try” blocks await t 0; try { await t 1; await t 2; } finally { Console. Write("f"); } await t 3; static public void Move. Next() { bool _fin = true; switch (_state ) { case 0: goto AFTERAWAIT 0; case 1, 2: goto STAGEPOST; case 3: goto AFTERAWAIT 3; case -1: /*fallthrough*/ } if (!t 0. Is. Completed ) {_state=0 ; _fin=false; return; AFTERAWAIT 0: _ state=-1; } STAGEPOST: try { switch (_state) { case 1: goto AFTERAWAIT 1; case 2: goto AFTERAWAIT 2; case -1: /*fallthrough*/ } if (!t 1. Is. Completed ) {_state=1 ; _fin=false; return; AFTERAWAIT 1: _ state=-1; } if (!t 2. Is. Completed ) {_state=2 ; _fin=false; return; AFTERAWAIT 2; _ state=-1; } } finally { if (_fin) { Console. Write("f"); } } if (!t 3. Is. Completed ) {_state=3 ; _fin=false; return; AFTERAWAIT 3; _ state=-1; } } 8. Stack spilling int[] a; int i; a[i]. Compare. To( await t); [[PUSH a]] ; (int[]) [[PUSH i]] ; (int[], int) DUP 0: DUP 1 ; (int[], int) LDELEM ; (int[], int) POP ; (int[], int) TUPLE. NEW ; (Tuple<int[], int>) STFLD this. _stack . . . RETURN ; do awaiter pattern. . . AFTERAWAIT 0: LDFLD this. _stack ; (Tuple<int[], int>) MAKE_STACK ; (int[], int) CALL t. Get. Result ; (int[], int) MAKE_LVALUES ; (&int, int) CALL Compare. To ; (bool) Async Codegen Lucian Wischik, VB Language PM These slides describe the IL that’s emitted when you use the Async and Await keywords in VB/C#.
1. State Machine async Task Foo. Async() { BODY } Perf tip: struct Foo. Async_State. Machine : IAsync. State. Machine { //(1, 2, 3) private int _state; public Async. Task. Method. Builder _builder; private void Move. Next() { try { switch (_state) { Task Foo. Async() { TRANSFORMED_BODY var sm = new Foo. Async_State. Machine(); } sm. _state = -1; } . . . copy params & this if needed into S. M. catch (Exception ex) { _builder. Set. Exception(ex); sm. _builder = Async. Task. Method. Builder. Create(); sm. _builder. Start(ref sm); return; return sm. _builder. Task; } } _builder. Set. Result(); } The async tranformation will add code and will add local variables. private void Set. State. Machine(IAsync. State. Machine sm) { If the JIT finds too much code or too many variables, then it _builder. Set. State. Machine(sm); } So, you’ll reach this JIT limit sooner with degrades drastically. } async methods. (1) The compiler implicitly generates a state machine for each async method. Each state corresponds to a piece of code between await statements. The Move. Next() method will advance it to the next state. (2) The state-machine is a struct, for efficiency reasons -- so that on the “fast path” where no awaits were actually needed, then it doesn’t need to be allocated on the heap. (3) The state machine implements System. Runtime. Compiler. Services. IAsync. State. Machine. This interface is part of the protocol for making it efficient and secure -- detailed later.
2. struct Async. Task. Method. Builder public struct System. Runtime. Compiler. Services. Async. Task. Method. Builder<T> { internal IAsync. State. Machine m_sm; public void Start<TSM>(ref TSM sm) where TSM : IAsync. State. Machine { //(2) Thread t = Thread. Current. Thread; Execution. Context. Switcher ecs = default(Execution. Context. Switcher); Runtime. Helpers. Prepare. Constrained. Regions(); try { Execution. Context. Establish. Copy. On. Write. Scope(t, false, ref ecs); sm. Move. Next(); } finally { ecs. Undo(current. Thread); } } } internal struct Void. Task. Result { } (2) We use structs a lot… The pattern "void Start<T>(ref T s) where T: I" lets us use a struct via an interface, but without boxing or copying the struct. This comes at the cost of JIT having to generate a new Start for each TSM. The state-machine is a struct, which contains Async. Task. Method. Builder which is a struct. At the first await point, the state machine will be boxed onto the heap, with the builder inside it, and the builder will get a pointer to that boxed state machine. It takes some work to maintain these circular references of structs. async void f() async Task<T> f() async Task f() - uses Async. Void. Method. Builder - uses Async. Task. Method. Builder<T> - uses Async. Task. Method. Builder, which wraps Async. Task. Method. Builder<Void. Task. Result>
3. Awaiter Pattern Console. Write. Line("a"); await Task. Delay(100); Console. Write. Line("b"); switch (_state) { case 0: goto AFTERAWAIT 0; case -1: // fallthrough } Console. Write. Line("a"); Task. Awaiter tmp = Task. Delay(100). Get. Awaiter(); //(4) if (!tmp. Is. Completed) { //(5) _state = 0; _awaiter = tmp; _builder. Await. On. Completed(ref tmp, ref this); //(8) return; AFTERAWAIT 0: //(9) tmp = (Task. Awaiter)_awaiter; _awaiter = default(Task. Awaiter); } tmp. Get. Result(); //(6) tmp = default(Task. Awaiter); //(7) Console. Write. Line("b"); (4) The await operator is pattern-based. For “await t”, the compiler makes a call to t. Get. Awaiter() to get an awaiter. For instance, you could make an extension method “My. Awaiter Get. Awaiter(this int i)” to be able to await integers – in which case tmp would have type My. Awaiter rather than Task. Awaiter. Win. RT uses this, so you can await an IAsync. Info. Perf tip: (5) In this case we awaited Task. Delay(100), which won’t have completed yet. But imagine if the task had already completed. Then it would go to calling tmp. Get. Result(), with no need for heap allocations. If straight every “await” is already completed, then it will avoid all heap apart from thetask final resultant Task. a. But (6) The job of tmp. Get. Result() is toallocations throw any exceptions from the (if any), and to return value (if any). the overhead is still about 2 x that of a non-async method. (7) We null-out the temporary variable immediately so it can be garbage-collected.
4. Await. On. Completed() class Async. Method. Builder<T>: public void Await. On. Completed<TA, TSM>(ref TA a, ref TSM sm) where TA : INotify. Completion where TSM : IAsync. State. Machine { if (m_sm == null) { var ignored = this. Task; // allocate a reference m_sm = (IAsync. State. Machine)sm; // box on first await m_sm. Set. State. Machine(m_sm); // tie up lose ends } Perf tip: Action cont; var ctx = Execution. Context. Fast. Capture(); if (ctx == Execution. Context. Pre. Allocated. Default) { cont = m_default. Context. Action; if (cont == null) cont = new Action(new Move. Next. Runner(ctx, this. m_sm). Run); } else { cont = new Move. Next. Runner(ctx, m_sm). Run; } If you avoid modifying Execution. Context, then it avoids further heap allocations and is faster. a. On. Completed(cont); } (8) Await. On. Completed ultimately calls awaiter. On. Completed(Move. Next)… First, if needed, it boxes the state machine on the heap, including the Async. Method. Builder struct. Set. State. Machine gives the builder a back-pointer to the S. M. Its two arguments are both ref parameters so that we don’t need to copy them to pass them. The builder backs up the Execution. Context before invoking Awaiter. Move. Next. And it allocates & passes a delegate (through class Move. Next. Runner) which will restore the Execution. Context. This is for security. It’s much cheaper if the Execution. Context is never changed.
5. struct Task. Awaiter class Task<T> { Task. Awaiter<T> Get. Awaiter() { return new Task. Awaiter<T>(this); } } struct Task. Awaiter<T> : INotify. Completion { private var m_delegates = new Concurrent. Queue<Action>(); public bool Is. Completed { get; } public T Get. Results() { return. . . ; } (12) The user expects the code after "await" to resume on the same Synchronization. Context. It's the job of each awaiter to save the sync. context, and execute its continuation on that context. Thus: the Async. Method. Builder takes care of saving and Perf tip: restoring Execution. Context (since that would be a security You’ll typically make Structure awaiters, toloophole avoid otherwise), and the unnecessary heap allocation. They should awaiter be takes care of saving and restoring immutable: mutations in On. Completed will be Synchronization. Context. public void On. Completed(Action cont) { //(12) var sc = Synchronization. Context. Current; m_delegates. Enqueue(delegate { sc. Post(_ => cont(), null); } } discarded if the state-machine gets boxed!
5. struct Task. Awaiter class Task<T> { Task. Awaiter<T> Get. Awaiter() { return new Task. Awaiter<T>(this); } } struct Task. Awaiter<T> : ICritical. Notify. Completion { private var m_delegates = new Concurrent. Queue<Action>(); public bool Is. Completed { get; } public T Get. Results() { return. . . ; } Perf tip: public void On. Completed(Action cont) { //(10) var ec = Execution. Context. Capture(); var sc = Synchronization. Context. Current; m_delegates. Enqueue(delegate { Execution. Context. Run(ec, delegate { sc. Post(_ => cont(), null); }); } (10) But if your awaiter is in fulltrust assembly which Allows. Partially. Trusted. Callers, then this would be a security hole. Therefore, your On. Completed method has to save and restore Execution. Context as well. (11) It would be inefficient if both Async. Method. Builder and your awaiter had to save+restore If you’re full-trust and allow partially-trusted Execution. Context. And so, if your callers, then use this Unsafe. On. Completedawaiter trick. implements ICritical. Notify. Completion, then [Security. Critical] //(11) public void Unsafe. On. Completed(Action cont) { the compiler will instead emit a var sc = Synchronization. Context. Current; call to the _builder method m_delegates. Enqueue(delegate { sc. Post(_ => cont(), null); }); Await. Unsafe. On. Completed, } which saves Execution. Context as } before. This will call the awaiter method Unsafe. On. Completed. class Async. Method. Builder<T>: public void Await. Unsafe. On. Completed<TA, TSM>(ref TA a, ref TSM sm) //(11) You’re at liberty here to avoid where TA : ICritical. Notify. Completion restorign Execution. Context, so where TSM : IAsync. State. Machine long as you mark your method { . . . exactly as above [Security. Critical] – this prevents a. Unsafe. On. Completed(cont); partially-trusted callers. }
3. Awaiter Pattern - dynamic Console. Write. Line("a"); await (dynamic)t; Console. Write. Line("b"); Console. Write. Line("a"); dynamic tmp = t. Get. Awaiter(); // dynamic method-call if (!tmp. Is. Completed) { // dynamic property-get _state = 0; _awaiter = tmp; var cnc = tmp as ICritical. Notify. Completion; // CLR cast, not dynamic if (cnc != null) { _builder. Await. Unsafe. On. Completed(ref cnc, ref this); } else { var nc = (INotify. Completion)tmp; // CLR cast, not dynamic _builder. Await. On. Completed(ref nc, ref this); } return; AFTERAWAIT 0: tmp = _awaiter; _awaiter = null; } tmp. Get. Result(); // dynamic method-call tmp = null; Console. Write. Line("b"); Perf tip: If you’re full-trust and allow partially-trusted callers, then use this Unsafe. On. Completed trick. In the case of a late-bound (dynamic) await call, it attempts to cast the awaiter first as ICritical. Notify. Completion and then as INotify. Completion. This is done using CLR casts rather than dynamic casts.
6. Lifted local variables int x = 10; await t 1; int z = 10; { int y = 15; Console. Write(x + y + z); } class Foo. Async_State. Machine: private int _x, _z; Move. Next: this. _x = 10; await t 1; this. _z = 10; { int y = 15; Console. Write(this. _x + y + this. _x); } Perf tip: Everything runsanmuch with variables. Ideally it’d work like this: “If a local is written before await, faster and read afterlocal an await, then it must be lifted into the state-machine (either by permanently locating it incompute-bound the state machine, orinner by putting there just before an await Factor out your loops into and restored afterwards, whichever is more efficient). ” separate methods, away from “await”, so they can runan faster. In practice: If a local’s scope includes await, then C# will permanently relocate the local into the state machine. This includes “z” in the above code. VB will relocate ALL locals into the state machine.
7. Nested “try” blocks await t 0; try { await t 1; await t 2; } finally { Console. Write("f"); } await t 3; static public void Move. Next() { bool _fin = true; switch (_state) { case 0: goto AFTERAWAIT 0; case 1, 2: goto STAGEPOST; case 3: goto AFTERAWAIT 3; case -1: /*fallthrough*/ } if (!t 0. Is. Completed) {_state=0; _fin=false; return; AFTERAWAIT 0: _state=-1; } STAGEPOST: try { switch (_state) { case 1: goto AFTERAWAIT 1; case 2: goto AFTERAWAIT 2; case -1: /*fallthrough*/ } if (!t 1. Is. Completed) {_state=1; _fin=false; return; AFTERAWAIT 1: _state=-1; } if (!t 2. Is. Completed) {_state=2; _fin=false; return; AFTERAWAIT 2; _state=-1; } } finally { if (_fin) { Console. Write("f"); } } if (!t 3. Is. Completed) {_state=3; _fin=false; return; AFTERAWAIT 3; _state=-1; } } If there are “try” blocks, then the compiler emits additional switch-blocks for each one. It’s illegal to jump straight into a TRY, so the compiler uses a “staging post” label to fall into the try. Also, VB iterators use the same Move. Next trick for iterators: when iterator’s Dispose method is called, it sets a field _is. Disposing=true in the state machine, and jumps into the Move. Next. Each switchblock is followed by a test “if (is. Disposing) then return false”, which will end up calling each finally block on the way out.
8. Stack spilling int[] a; int i; a[i]. Compare. To(await t); [[PUSH a]] ; (int[]) [[PUSH i]] ; (int[], int) DUP 0: DUP 1 ; (int[], int) LDELEM ; (int[], int) POP ; (int[], int) TUPLE. NEW ; (Tuple<int[], int>) STFLD this. _stack . . . RETURN ; do awaiter pattern. . . AFTERAWAIT 0: LDFLD this. _stack ; (Tuple<int[], int>) MAKE_STACK ; (int[], int) CALL t. Get. Result ; (int[], int) MAKE_LVALUES ; (&int, int) CALL Compare. To ; (bool) Normally, the compiler will evaluate each sub-expression in turn (pushing it onto the stack), then it will call the desired Perf tip: operation. If there is an “await” which. You doesn’t the fast-path, it’ll have to save the stack into the state machine before cantake typically do a then better job of stack-spilling returning. That’s because the stack has to be empty when at each RETURN operaton. The compiler saves the stack into than the compiler. Avoid “await” that’s nested a Tuple of the appropriate type, stored in a state-machine field “_stack” of type Object. deep inside expressions. Use it mostly in But if there were any managed addresses where on the stack, then first this isn’t allowed. In to thebe above code, it would normally statements it’s the expression want to evaluate a[i] as a managed address (lvalue) before calling the Compare. To method; all struct methods are evaluated, e. g. similarly invoked on managed addresses in case the method mutates the struct. var x = await t; using (await t) {…} In such cases the compiler has to avoid pushing the managed address onto the stack in the first place. Instead it pushes the constituent parts (in this foreach case “a” and “i”). i in These can be (var await t) saved {…} into the Tuple okay. Later on, after the “await” has finished and immediately prior to the call to “Compare. To”, it reconstitutes those constituent parts into the address. Note: it still had to issue a dummy LDELEM call in advance, just to shake out any Array. Index. Exceptions that might arise. Managed addresses can come only from “LOCAL”, “rvalue. FIELD”, “rvalue[rvalue]” and from By. Ref parameters (disallowed in async methods). Managed addresses are only ever consumed by “lvalue. M(…)”, “lvalue=rvalue”, “lvalue++”, and passing an lvalue By. Ref.
- Slides: 11