Why Thread-Local State Gets Lost After Await
Press β or Space to navigate
ACCESS_DENIED (0x5)RevertToSelf() returning falseThreadLocal<T>, Impersonation tokens, Culture settingsImpersonateLoggedOnUser(token) β Sets impersonation on CURRENT thread
RevertToSelf() β Clears impersonation on CURRENT thread
Thread A: [Your Code]
β
Set impersonation
β
Start async work
β
await β yields here
β
(Thread A returns to pool)
Thread B: (picked from pool)
β
Continuation code
β
Finally block
β
RevertToSelf() β Wrong thread!
| Thread A | Thread B |
|---|---|
| β
Has impersonation set π Returns to pool (STILL impersonated!) |
β No impersonation β οΈ RevertToSelf() does nothing |
await does NOT pause the thread. It releases the thread back to the pool and schedules a continuation to run when the async operation completes.
The ThreadPool doesn't care about thread-local state. It just grabs the first available thread to run your continuation. That thread knows nothing about what Thread 1 was doing.
ββββββββββββ¬βββββββββββββββββββββ
β Thread 1 β Thread 12 β
ββββββββββββΌβββββββββββββββββββββ€
β π΄User1 ββ
No Impersonation β β Thread 1 is DIRTY!
ββββββββββββ΄βββββββββββββββββββββ
| User | Thread State | Result | After |
|---|---|---|---|
| User 1 | Clean | β Success | Thread dirty |
| User 2 | Dirty | β Fail + Cleanup | Thread clean |
| User 3 | Clean | β Success | Thread dirty |
| User 4 | Dirty | β Fail + Cleanup | Thread clean |
cd c:\Users\zhenxingl\upm\AppxPreRegisterTool\AsyncThreadSwitchDemo
dotnet run
await[User1] Thread 1: Set thread-local state to 'User1'
[User1] Thread 12: β οΈ THREAD SWITCH! Was on Thread 1
[User1] Thread 12: β State LOST!
[User1] Thread 12: β CLEANUP FAILED! No state to clear.
// β WRONG: async method with thread-local state
static async Task ProcessUserAsync(string userName)
{
try
{
ThreadLocalState.Value = userName; // Thread A: Sets state
await Task.Delay(100); // β οΈ THREAD SWITCH HAPPENS HERE!
// Now on Thread B - state is LOST!
}
finally
{
ThreadLocalState.Value = null; // Thread B: Nothing to clear!
}
}
await keyword allows the thread to switch,
but the finally block expects to run on the same thread that set up the state.
// β
CORRECT: Synchronous wrapper keeps impersonation on same thread
static void ProcessUser(string userName) // No async!
{
ImpersonateLoggedOnUser(userToken); // Thread A
try
{
// Call async work and BLOCK - do NOT await here!
DoAsyncWorkAsync().Wait(); // Async work runs on thread pool
}
finally
{
RevertToSelf(); // Thread A - SAME thread! β
}
}
// Async work is isolated - no impersonation concerns here
static async Task DoAsyncWorkAsync()
{
await Task.WhenAll(registrationTasks);
// Thread switches are fine here - no thread-local state
}
| Thread | Action |
|---|---|
| Thread A | ImpersonateLoggedOnUser() - sets impersonation |
| Thread A | Calls DoAsyncWorkAsync().Wait() - BLOCKS but stays on Thread A |
| Thread B/C/D | Async work runs on thread pool (no impersonation needed) |
| Thread A | finally block runs - RevertToSelf() on SAME thread β
|
.Wait() instead of await causes the calling thread to block but not yield.
The thread stays parked until the async work completes, guaranteeing the finally block runs on the same thread.
Code before await and code after await may run on different threads
Impersonation, ThreadLocal<T>, and other thread-bound state stays on the original thread
Cleanup code in finally cannot access the original thread's state
Keep thread-local setup/cleanup in a synchronous wrapper, isolate async work in separate functions
// π¨ DANGEROUS: Setting thread state before await
async Task DoWork()
{
SetThreadState(); // impersonation, culture, etc
try
{
await SomeAsyncOperation(); // Thread switch happens here!
}
finally
{
ClearThreadState(); // Wrong thread!
}
}
ImpersonateLoggedOnUser() / RevertToSelf()ThreadLocal<T>Thread.CurrentPrincipalThread.CurrentCultureasync Task Bad()
{
Impersonate();
try
{
await Work();
}
finally
{
Revert(); // Wrong thread!
}
}
void Good()
{
Impersonate();
try
{
Work().Wait();
}
finally
{
Revert(); // Same thread!
}
}
AsyncLocal<T> instead of ThreadLocal<T> if state must flow with asyncConfigureAwait(true) does NOT guarantee same thread - don't rely on it!| Concept | Key Point |
|---|---|
| Problem | Async/await switches threads; thread-local state stays on original thread |
| Symptom | Alternating failures (odd users work, even users fail), ACCESS_DENIED |
| Root Cause | Cleanup runs on different thread than setup |
| Solution | Encapsulate async work; keep impersonation in sync wrapper |
| Prevention | Never await inside impersonation try/finally block |
AsyncThreadSwitchDemo/AsyncThreadSwitchDemo/README.mdvoid ProcessUser() // Sync wrapper
{
Impersonate();
try
{
AsyncWork().Wait(); // Block here
}
finally
{
RevertToSelf(); // Same thread!
}
}
"The best bug fixes are often one line of code,
but finding that line requires understanding the whole system."