Threads and thread-safety
“What is a thread, and what makes code thread-safe?”
One line of work the computer runs. A web app runs many at once, and they all share your singletons.
See it
In plain words
A thread is one line of work: the computer runs its code one statement after another. A web app runs many threads at once, one for each busy request. If those threads share one object and two change it at the same moment, they spoil each other's work. Thread-safe code still works correctly when that happens.
- A thread is the unit of execution the OS schedules.
- ASP.NET Core runs each request on a thread-pool thread, so requests run in parallel, and any in a singleton or a field is shared by all of them.
- A type is thread-safe if concurrent calls cannot corrupt it.
- is not safe for concurrent writes: under load it silently loses entries or throws.
- Fix it with or a , or better, don't share changing state.
- frees the thread while I/O is pending.
In the lending API
The bug, the fix, and the test that catches it. Press Next line to walk through them.
Lending.sln · where these 3 files sit
- src/Infrastructure/Servicing/
- tests/Infrastructure.UnitTests/
What they ask next
Interviewers check that you can explain every line. Tap a question to open it.
What is a race condition?
A bug where the result depends on which thread gets there first. Here, two threads both take 7, and whichever writes second wins.
Explain this lineWhy are reads safe on a Dictionary, but not writes?
A read only looks. A write moves things: when the dictionary is full it builds bigger arrays and copies every entry across. A thread that reads or writes in the middle of that sees a half-built dictionary.
Why not…Why not wrap the Dictionary in a ?
That works.
lock (_gate) { _loans.Add(loan.Id, loan); }lets one thread in at a time. Every read and write must take the same lock, or it protects nothing. uses smaller locks and lock-free reads, so threads wait less.What can go wrong with locks?
Deadlock: two threads each hold one lock and wait forever for the other's. Also, you can't inside a . In async code, use and
await WaitAsync(). On .NET 9 and later, lock onprivate readonly Lock _gate = new();, not a plain object.
Does make my code thread-safe?
Each single call is safe. Two calls in a row are not:
if (!d.ContainsKey(k)) d[k] = v;can still race. Use the one-step methods: , , . Careful: under a race,GetOrAdd(k, factory)can run the factory twice, though only one result is stored. Wrap costly work in . And the objects inside are not protected: two threads calling on one loan still race.ReportedA singleton counts requests with
_count++. Is that safe?No.
_count++is three steps: read, add 1, write. Two threads read 41 and both write 42, so a count is lost. does it as one step.Why does a static throw "There is already an open DataReader"?
One connection is shared by every request thread. Request A is still reading rows when request B runs its own query on the same connection, and a connection allows one open reader at a time (MARS lifts that, but the connection is still not thread-safe). A connection, like EF Core's , is not thread-safe: open one per request. Connection pooling makes that cheap.
Is a scoped service thread-safe?
It usually doesn't need to be. Each request gets its own object, used by one request at a time. That is why is scoped. Two ways it still gets shared: a singleton that captures a scoped , or on one context inside one request. EF Core then throws "A second operation was started on this context". Singletons and statics are shared, so they must be thread-safe.
What does do to the thread?
While the request waits for a database or an HTTP call, gives the thread back to the pool to serve other requests. The rest of the method resumes later, often on a different thread. Async and await get their own page later.
What if code blocks with
.Resultinstead?The thread sits idle, waiting, and can serve no one. Under load every pool thread ends up stuck like that and new requests queue: thread-pool starvation. The app looks slow, while CPU stays low.
How many threads does ASP.NET Core use?
As many as the thread pool decides. It starts with about one per CPU core and adds more slowly when all are busy. You don't create threads per request yourself.
Why was this bug hard to find?
Tests call one method at a time, so the race never happens. In production it needs two writes within microseconds, so it shows up only under load, now and then, and never on a laptop. The report reads: "Customer got 201 Created, then loan not found."
Traps
- Don't say
Each request has its own thread, so my objects are safe.SayEach request has its own thread, but a singleton or a static is one object for all of them. Shared data that changes needs a thread-safe type or a lock. - Don't say
It passes my tests, so it's thread-safe.SayNormal tests run one call at a time. Prove it with a concurrent test, like with thousands of writes. - Don't say
starts a new thread.Say gives the current thread back while it waits. No thread is used while a database or HTTP call is in flight.
AI tools often write an in-memory repository or cache as a plain or inside a singleton. It compiles and passes one-user tests. Interviews now include reviewing AI-written code, and "this shared collection is not thread-safe" is the catch they expect.