In C#, this is defined by the current thread's synchronization context.
If you're writing a console application, the main thread doesn't have one. When an async function wants to resume, it will usually resume on some thread pool's thread. Potentially different one each time. Can be any other thread too, the runtime just resumes running the function on the same thread which completed the await.
But if you're writing a GUI app and launching an async function from its GUI thread, that thread has a current sync.context. When the function will want to resume running or fails, the runtime will resume/raise exception on the same thread where it started. More precisely, the runtime delegates the decision to the sync.context, and the contexts set by GUI frameworks choose to resume on the GUI thread.
This may cause some funny deadlock bugs, but in most cases works surprisingly well in practice.
Also it's easy to implement custom synchronization contexts if needed.
If you're writing a console application, the main thread doesn't have one. When an async function wants to resume, it will usually resume on some thread pool's thread. Potentially different one each time. Can be any other thread too, the runtime just resumes running the function on the same thread which completed the await.
But if you're writing a GUI app and launching an async function from its GUI thread, that thread has a current sync.context. When the function will want to resume running or fails, the runtime will resume/raise exception on the same thread where it started. More precisely, the runtime delegates the decision to the sync.context, and the contexts set by GUI frameworks choose to resume on the GUI thread.
This may cause some funny deadlock bugs, but in most cases works surprisingly well in practice.
Also it's easy to implement custom synchronization contexts if needed.