Asynchronous methods depend on their context for the exact semantics of their resumption after an await. A UI context is usually single threaded, which means that only one asynchronous operation can be executing on it at any given time. This is very useful in a stateful environment such as a UI, because less care needs to be taken to prevent against races between different operations simultaneously manipulating the UI state. No locks are needed (nor would they be useful!); operations just need to make sure that they finish their changes in one go, before they relinquish the thread.
With asynchronous operations, relinquishing the thread occurs not just at the end but also at every await. This means that an asynchronous UI operation needs to be prepared for the global state to look different after each await. This is a good thing: remember that we are explicitly putting the await’s there precisely so that other operations can get in and have an effect!
Another context that is fundamentally single threaded is the one in which ASP.NET requests are handled. While multiple requests can be processed concurrently, each request is executed sequentially even in the presence of asynchronous operations. Again this simplifies the interaction between the different operations collaborating to respond to the request, by obviating the need for locking to protect against races on the state of the request.
Other contexts are not single threaded. Most notably the thread pool context is multithreaded – a shared queue will feed jobs to whichever thread pool thread happens to free up next. This goes also for the method resumptions – the continuations – that get scheduled to it once their awaited task completes. This means that an async method that gets called on a thread pool thread may resume on different thread pool threads after each await. This is usually not a problem, but means that it should not rely on thread local state to remain accessible across await’s. In particular, locks are not to be trusted, and the compilers explicitly disallow await expressions in lock statements (SyncLock in Visual Basic).
Another aspect of the context is that void returning async methods will let the context know when they start and when they complete. Most contexts ignore this, but ASP.NET will use that to keep a count of ongoing asynchronous operations. That way it can hold back on moving on to the next phase in the pipeline until all top-level asynchronous operations –the void ones – are completed.
It is possible to create your own context by deriving from SynchronizationContext or TaskScheduler but it is not recommended except for very advanced scenarios where you really need to control your own scheduling.