Async demystified NET Core Summer event 2019 Brno
Async demystified. NET Core Summer event 2019 – Brno, CZ Karel Zikmund – @ziki_cz
Agenda • History of async patterns in. NET • History and evolution of • Task • async-await
APM pattern: Asynchronous Programming Model. NET Framework 1. 0/1. 1 … 2002 -2003 interface IAsync. Result { bool Is. Completed; bool Is. Completed. Synchronously; object Async. State; Wait. Handle Async. Wait. Handle; } Across BCL: IAsync. Result Begin. Foo(. . . , Async. Callback callback, object state); void End. Foo(IAsync. Result iar);
APM pattern IAsync. Result Begin. Foo(. . . , Async. Callback callback, object state); void End. Foo(IAsync. Result iar); Synchronous call: Foo(); Achieving the same: End. Foo(Begin. Foo(. . . , null)); Leveraging asynchronous calls: Begin. Foo(. . . , iar => { T val = End. Foo(iar); // do stuff. . . });
APM – Example Copy stream to stream: int bytes. Read; while ((bytes. Read = input. Read(buffer)) != 0) { output. Write(buffer, 0, bytes. Read); }
APM – Nesting problem Begin. Read(. . . , iar => { int bytes. Read = End. Read(iar); input. Begin. Write(. . . , iar 2 => { int bytes. Written 2 = End. Write(iar 2); Begin. Read(. . . , iar 3 => { int bytes. Read 3 = End. Read(iar 3); Begin. Write(. . . , iar 4 => { //. . . again and again }); });
APM – Is. Completed. Synchronously IAsync. Result r = Begin. Read(. . . , iar => { if (!iar. Is. Completed. Synchronously) { //. . . asynchronous path as shown earlier } }); if (r. Is. Completed. Synchronously) { //. . . Synchronous path } • Even worse in loop • Overall very complicated • Queueing on Thread. Pool much simpler
EAP: Event-based Asynchronous Pattern • . NET Framework 2. 0 obj. Completed += (sender, event. Args) => { //. . . my event handler } obj. Send. Packet(); // returns void • Did not solve multiple-calls problem, or loops • Introduced context
Task • . NET Framework 4. 0 • MSR project – parallel computing • Divide & conquer efficiently (e. g. Quick. Sort) • Shaped Task – similar to today • Task – represents general work (compute, I/O bound, etc. ) = promise / future / other terminology • Task / Task<T> – operation (with optional result T) 1. T … in the case of Task<T> 2. State related to synchronization 3. State related to callback
Task / Task. Completion. Source • Task • Here's a callback, invoke it when you're done, or right now if you've already completed • I want to block here, until your work is done • Cannot be completed by user directly • Task. Completion. Source … wrapper for Task • Holds Task internally and operates on it via internal methods • Methods: • Set. Result • Set. Exception • Set. Cancelled
Task – Consumption Task<T> t; Either: t. Wait(); // Blocks until Task is completed Or: t. Continue. With(callback); // Will be executed after Task is completed Even multiple times: t. Continue. With(callback 2); t. Continue. With(callback 3); Continue. With: • Does not guarantee order of executions • Always asynchronous (queued to Thread. Pool/scheduler in general)
Task. Run We complicated things Task<T> Task. Run(delegate d) • Adds field to Task with ‘d’ • Queues work to Thread. Pool • Thread grabs it, executes it, marks task completed
Task. Run implementation Task<T> Run(Func<T> f) { var tcs = new Task. Completion. Source<T>(); Thread. Pool. Queue. User. Work. Item(() => { try { T result = f(); tcs. Set. Result(result); } catch (ex) { tcs. Set. Exception(ex); } }); return tcs. Task; }
async-await. NET Framework 4. 5 / C# 5 Example of asynchronous code: Task<int> Get. Data. Async(); Task Put. Data. Async(int i); Code: Task<int> t = Get. Data. Async(); t. Continue. With(a => { var t 2 = Put. Data. Async(a. Result); t 2. Continue. With(b => Console. Write. Line("done")); });
async-await Task<int> t = Get. Data. Async(); t. Continue. With(a => { var t 2 = Put. Data. Async(a. Result); t 2. Continue. With(b => Console. Write. Line("done")); }); C# 5 with async-await helps us: Task<int> t = Get. Data. Async(); int a. Result = await t; Task t 2 = Put. Data. Async(a. Result); await t 2; Console. Write. Line("done");
Awaiter pattern int a. Result = await t; Translated to: var $awaiter 1 = t. Get. Awaiter(); if (! $awaiter 1. Is. Completed) { // returns bool //. . . } int a. Result = $awaiter 1. Get. Result(); // returns void or T // If exception, it will throw it
Awaiter pattern – details void Move. Next() { if (__state == 0) goto label 0; if (__state == 1) goto label 1; if (__state == 42) goto label 42; if (! $awaiter 1. Is. Completed) { __state = 42; $awaiter 1. On. Completed(Move. Next); return; } label 42: int a. Result = $awaiter 1. Get. Result(); }
State Machine string x = Console. Read. Line(); int a. Result = await t; Console. Write. Line("done" + x); State machine: struct Method. Foo. State. Machine { void Move. Next() {. . . } local 1; // would be ‘x’ in example above local 2; params; _$awaiter 1; }
State Machine – Example public async Task Foo(int timeout) { await Task. Delay(timeout); } public Task Foo(int timeout) { Foo. State. Machine sm = default; sm. _timeout = timeout; sm. _state = 0; sm. Move. Next(); return ? ? ? ; } struct Foo. State. Machine { int _timeout; // param // locals would be here too void Move. Next() {. . . } int _state; Task. Awaiter _$awaiter; }
State Machine – Example public Task Foo(int timeout) { Foo. State. Machine sm = default; sm. _tcs = new Task. Completion. Source(); sm. _timeout = timeout; sm. _state = 0; sm. Move. Next(); return sm. _tcs. Task; } Async. Value. Task. Method. Builder. Create(); _tcs. Task -> _builder. Task; struct Foo. State. Machine { int _timeout; // param // locals would be here too void Move. Next() { //. . . _tcs. Set. Result(. . . ); } int _state; Task. Awaiter _$awaiter; Task. Completion. Source _tcs; }
State Machine – Summary What about Task allocation? • Builder can reuse known tasks • • Task. Completed. Task (without value) boolean – True/False int … <-1, 8> Last. Completed (e. g. on Memory. Stream) • Does not work on Ssl. Stream (alternates headers and body) • Size: 64 B (no value) / 72 B (with value) • Azure workloads OK (GC will collect) • Hot-path: up to 5%-10% via more GCs
Value. Task • . NET Core 2. 0 • Also as nuget package down-level struct Value. Task<T> { T; Task<T>; } • Only one of them: T+null or default+Task<T> • NET Core 2. 1 Value. Task<int> Stream. Read. Async(Memory<byte>, . . . )
Value. Task – Can we improve more? • What about the 1% asynchronous case? • . NET Core 2. 1 struct Value. Task<T> { T; Task<T>; IValue. Task. Source<T>; } struct Value. Task { Task; IValue. Task. Source; }
Summary • APM pattern = Asynchronous Programming Model • . NET Framework 1. 0/1. 1 (2002 -2003) • IAsync. Result, Begin. Foo/End. Foo • Limited nesting / loops • EAP = Event-based Asynchronous Pattern (. NET Framework 2. 0) • Events – similar problems as APM • Task (. NET Framework 4. 0) • Wait / Continue. With • Task. Completion. Source (for Write) • async-await (. NET Framework 4. 5 / C# 5) • Awaiter pattern, state machine • Value. Task (. NET Core 2. 0) • Don’t use unless you are on hot-path • Hyper-optimizations possible, stay away! @ziki_cz
- Slides: 24