TechEd North America 2014, Houston
Async Best Practices for C# and VB – Mads Torgersen
Day 4, 15 May 2014, 10:15AM-11:30AM (DEV-B362)
Disclaimer: This post contains my own thoughts and notes based on attending TechEd North America 2014 presentations. Some content maps directly to what was originally presented. Other content is paraphrased or represents my own thoughts and opinions and should not be construed as reflecting the opinion of either Microsoft, the presenters or the speakers.
Executive Summary—Sean’s takeaways
- Avoid async void
-
Don’t use parallel threads for I/O bound code
- Don’t want to waste threads in thread pool by having them waiting on I/O
-
Avoid event handler mess by using async/await
- Use TaskCompletionSource to hook event to lambda that sets result of task
- Don’t wrap synchronous code in async method
- Don’t block UI thread to wait for completion of asynchronous code
Mads Torgersen– Program Manager, C# Language, Microsoft
One of the people responsible for the async feature
Key takeaways
- Async void is only for top-level event handlers
- Use threadpool for CPU-bound code, but not IO-bound
- Use TaskCompletionSource to wrap Tasks around events
- Libraries shouldn’t lie, and should be chunky
Async void is only for event handlers
- User: if user clicks Print button too quickly, stuff not ready
Stop using async void
- Unless you absolutely have to
Async void only for event handlers
- Event handlers are async void
-
In your handler, you might call your own async void method
- Then in 2nd method, you hit await
- That method returns
- Event handler then also awaits
- ** slide – with arrows **
- Can’t predict which order the methods will resume in
Variant #2 – Exception in GetResponseAsync
- Exception doesn’t come back to original caller
- async void method has no Task to put exception in
- Then UI thread crashes, because no handler
How to fix
- Make 2nd function Task return, then await it
Example 3 – virtual methods returning void
- E.g. override of OnNavigatedTo, LoadState
- The override calls base
- But OnNavigated base calls overridden LoadState
- Again, a race condition because we hit awaits and exit methods
-
Solution
- Can’t return Task
- Still hand off Task from caller to callee
- Stick result of 2nd call in variable, after calling async Task helper method
- Then in 1st method, can await on the variable!
- Brilliant
- Tasks always the best way to communicate completion
Example 4 – Can’t always see when you’re doing async void
- Lambda
- Lambdas may map to delegate that returns void
- If both overloads offered, it picks Task-returning
-
E.g. If you Dispatcher.RunAsync with async lambda
- When lambda returns, the caller thinks that work is done
-
Sol’n: find another way to communicate completion
- Factor it out into async method that returns Task
- Search for async (), check it
Async void only for event handlers
-
Principles
- Fire-and-forget mechanism—almost never what you want
- Caller unable to know when async void has finished
- Caller unable to catch exceptions
-
Guidance
- Use only for top-level event handlers
- Use async Task-returning methods everywhere else
- If you need fire-and-forget, be explicit
- When you see async lambda, verify it
Threadpool
-
User: how should I parallelize my code
- Loading list of housing data
- Use Threadpool, task parallel library, parallel for?
-
Diagnose/Fix
- Users code was not CPU-bound
Threadpool – sequential
- Sequentially, you have to wait
Threadpool – Try #1
-
Parallel.For
- Deserialize and add to list
- Runs on parallel cores
- Now down to 300ms, from 500ms
-
But is it really taking 100 ms per house?
- Nope, it’s actually I/O bound
- Deserialize is actually blocking on I/O
Is it CPU-bound or I/O-bound?
- CPU-bound – should parallelize
- I/O-bound – maybe not
How it works
- Doing two threads, two cores
- Gradually spins up threads, as it sees first thread waiting on I/O
- So we created more threads than we need
How to code it right
- Parallelize I/O bound code
- List of tasks
- LoadFromDatabaseAsync
- Then: await Task.WhenAll(tasks)
- No threadpool
- Thread should not be waiting on I/O
Threadpool – may get another bottleneck?
- Moving off threadpool, but doing I/O in parallel may lead to I/O bottleneck if you have a large number of tasks
- So use a queue and workers
- Use WorkerAsync
- Create three workers
Threadpool
-
Principles
- CPU-bound okay on threads
- Parallel.ForEach and Task.Run are good way to put CPU-bound work onto thread pool
- E.g. LINQ-over-objects, computational
- Use of threads won’t increase throughput on machine that’s under load
-
Guidelines
- For IO-bound work, use await, rather than background threads
- For CPU-bound work, use background threads via Parallel.ForEach or Task.Run, unless you’re writing library or scalable server-side code
Async over events
-
User
- UI code looks like spaghetti
-
Diagnose/Fix
- Events are the problem
- Consider wrapping them as Tasks
Apple picking game
- Multiple levels of events
- Sequence of things all listed as nested lambdas
- Ick !
- Callback hell
-
Solution
- State machine
- Becomes complicated in a new way
- Now we have everything as global events
The problem is events—they’re not going away
Solution
- await async events
- Trick – how to turn helper methods into async
Async over events – async helper methods
- Use TaskCompletionSource<object>
- Guy who creates TCS controls how task completes
- Make your own task, rather than letting async create a task for you
- Lambda just tells task that things are done
- Then wire this lambda into Completed event
- Then you await this tcs.Task
- So—it’s about converted synchronous work with Completed handler into Task-based paradigm
- Fantastic!
Async keyword is for creating logic around methods that are already async
- When you want to create your own Task, use TaskCompletionSource
Wow, this is great. Learning async tricks from Mads..
Async over events
-
Principles
- Callback-based programming, as with events, is hard
-
Guidance
- If event handlers are largely independent, leave them as events
- If they look like state machine, then await is maybe easier
- To turn event into awaitable tasks, use TaskCompletionSource
Library methods shouldn’t lie
- Signature hints at whether a method is synchronous or asynchronous
Library methods shouldn’t lie
-
Honest about synchronous
- Some methods do actual work, occupy work
- You should say so, synchronous
-
Synchronous methods
- Do work
- You’re not wasting your time waiting for me
-
Asynchronous methods
- I’ll initiate something, but return immediately
Library methods shouldn’t lie – Example
-
Synchronous
- PausePrint – burns CPU
-
Asynchronous
- PausePrintAsync – await Task.Delay(10000)
- Honest, because it returns immediately, spawns task
-
Don’t: wrap synchronous code in async method
- Returns Task.Run (return it)
- This method lies—it’s not really async
-
Never: wrap asynchronous code in synchronous
- Async method that returns synchronously
- Synchronous wrappers for asynchronous work – NO !
Dangers of wrapping synchronous in asynchronous method
-
Wrap synchronous in async
- You’re still doing work
- Hiding from app dev where work is being done
-
Threadpool is app-global resource
- Scalability hurt
- On server, spinning up threads hurts scalability
-
App is the best position to manage its threads
- Don’t use threads in secret
Dangers of blocking – wrap asynchronous in synchronous
- LoadAsync
- Then wait on this in button click handler
- And then update view
- Rather than doing await so that handler returns immediately
- LoadAsync works fine—creates thread
- Blocks UI thread—bad !
- Then LoadAsync does await and it leads to deadlock
- Because the resumption of LoadAsync, after await, wants UI thread
- But you’ve blocked UI thread—crap
- A bit better in thread pool
Library methods shouldn’t lie
-
Principles
- Threadpool is app-global resource
- Poor use of threadpool hurts scalability
-
Guidance
- Help callers understand how your method behaves
- Libraries shouldn’t use threadpool in secret
- Use async signature only for truly async methods
Async – not spinning on threads
Sync – not blocking threads
Libraries should expose chunky async APIs
-
We all know sync methods are “cheap”
- Years of optimizations around sync methods
- Enables refactoring at will
-
E.g. synchronous string out
- IL is simple
-
Async method that outputs string (but doesn’t wait)
- Body of method is 3x longer
- Has to initialize state machine
- Lots of plumbing
-
Important mental model
- How many allocations are required for async state machine?
- Allocation will eventually require GC
- Garbage collection is what’s costly
Fast Path in awaits
-
Each async method has to allocate
- State machine class holding method’s local variables
- Delegate
- Returned Task object
-
If code path doesn’t hit any awaits
- Optimized so that state machine and delegate aren’t allocated. Just Task
- If awaited Task has already completed, then skip actual wait
-
If you don’t have any awaits fired and you have common result (e.g. 0, 1, true, false, null, “”)
- Compiler just grabs these pre-gen’d task and just returns it
-
You can follow this same pattern, for common return values in Tasks
- Create wrapper
Libraries should expose chunky async APIs
-
Principles
- Heap is an app-global resource
-
Guidance
- Libraries should expose chunky async APIs, not chatty
-
If library has to be chatty, and GC perf is problem, and heap has lots of async allocations
- Then optimize the fast-path
- Generally, use async to your heart’s content and don’t worry about it
- But just be aware of what’s going on under the hood
Consider .ConfigureAwait(false) in libraries
-
Sync context represents a “target for work”
- E.g. DispatcherSynchronizationContext, whose .Post() does Dispatcher.BeginInvoke()
- Sort of “where do I live”
- When await resumes execution, it has to look at sync context to figure out what thread to run code on. E.g. On UI thread
- Goal: after await, you should be where you were before (e.g. I’m still on the UI thread)
- Extra level of bookkeeping can be expensive
-
Library code often doesn’t care where it’s running
- You can ask await to not go find original context after await, but just keep running in whatever context is current
-
“Await task” uses the sync context
- await task.ConfigureAwait(false)
- Suppresses SyncContext.Post()
Consider .ConfigureAwait(false)
-
Principles
- UI message-queue is app-global resource
- Too much use will hurt UI responsiveness
-
Guidance
- If you call chatty async APIs but doesn’t touch the UI, use ConfigureAwait(false)
Resources for async Best Practices
Reblogged this on .Net Development Stack.