C# & .NET (C# 14, .NET 10 LTS) Module 16
Threads, parallelism and synchronisation interview questions
Interview questions on threads, parallelism and synchronisation: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
What is the difference between a foreground and a background thread in .NET?
Answered in the lesson Threads, the thread pool and async-local state
-
Fresher
Why is count++ not thread-safe in C#?
Answered in the lesson Locks, Interlocked and the .NET memory model
-
Fresher to mid
What does a bounded channel do when it is full?
-
Fresher to mid
What is the difference between limiting concurrency and limiting rate?
Answered in the lesson Throttling with SemaphoreSlim and rate limiters
-
Fresher to mid
When does Parallel.For make a loop slower?
Answered in the lesson Parallel.For, Parallel.ForEach and PLINQ
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
A queue worker starts a new Thread for every message it receives. At 2,000 messages a minute its memory keeps rising and messages wait longer and longer. What is going on, and how do you move it to the thread pool without breaking the messages that block?
- What it tests
- The cost of threads against the pool, telling short CPU work from blocking work and I/O, and what changes for errors and shutdown after the move.
- First step
- Find out what a message does: short CPU work, a blocking call or an I/O wait. 2,000 a minute is about 33 a second; if each one blocks for seconds, threads are created faster than they finish, and each holds its own stack.
- Follow-ups
-
- How many pool threads will the short messages need?
- What happens to a message whose handler throws after the move?
- A few messages block for 30 s on a driver with no async API. Where do they go?
From the lesson Threads, the thread pool and async-local state
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Log lines from one user's checkout show another user's request id. The id is kept in a [ThreadStatic] field that is set where each request starts, and the handlers are async. Explain the bug and fix it.
- What it tests
- Thread-local state against async flows, how AsyncLocal values flow into calls and not back, and keeping a request's id out of background work.
- First step
- Notice that an async handler can resume on another pool thread after every await, and that each pool thread runs many requests in turn: a [ThreadStatic] id belongs to a thread, not to a request.
- Follow-ups
-
- Where do you set the AsyncLocal, and who may change it?
- A cache refresh started by one request logs that request's id for every user. Why, and what do you change?
- How do you test that ids never mix?
From the lesson Threads, the thread pool and async-local state
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A payment service hangs a few times a week: two threads are stuck, and every later request waits behind them. Transfer locks the payer's account, then the payee's. Explain the hang and fix it so that it cannot happen again.
- What it tests
- Recognising a lock-order deadlock, the fix that rules it out for good, detecting the next one with time limits, and the cost of the alternatives.
- First step
- Look for two transfers in opposite directions between the same two accounts: each holds its first lock and waits for the other's. Then list every code path that takes two account locks, and the order in which each one takes them.
- Follow-ups
-
- How would you detect the next deadlock instead of a hang?
- A refund path locks the two accounts in the reverse order. What do you do?
- Would one lock for all accounts be better?
From the lesson Locks, Interlocked and the .NET memory model
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate fixed a lost-update bug by marking a counter field volatile. The nightly test with eight threads still counts too few. Explain why volatile did not help, and what each correct fix costs.
- What it tests
- What volatile guarantees under the .NET memory model and what it does not, atomic updates with Interlocked, and when a lock is needed instead.
- First step
- Separate ordering from atomicity: volatile makes each read and each write of the field fresh and ordered, but count++ is still a read, an add and a write that another thread can come between.
- Follow-ups
-
- Where does volatile help at all?
- The counter is a long on a 32-bit device. What changes?
- Two counters must always agree with each other. What do you use?
From the lesson Locks, Interlocked and the .NET memory model
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
At start-up a service logs that it loaded its pricing settings 8 times, and some requests used other settings than others. The property is written as if (_settings == null) _settings = Load(). Explain it and replace it.
- What it tests
- Check-then-act races in lazy initialisation, Lazy<T> and its modes, when double-checked locking is correct in .NET, and failures during the load.
- First step
- Count the threads that can pass the null test before the first Load returns: every request that arrives while it runs. Each builds its own settings and keeps using the object it built.
- Follow-ups
-
- Load sometimes throws. Which Lazy<T> mode do you pick, and why?
- Is a double-checked lock a correct alternative?
- The settings must be reloaded every hour. What changes?
From the lesson Locks, Interlocked and the .NET memory model
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A currency service caches exchange rates in a ConcurrentDictionary with GetOrAdd(code, LoadRates). During a traffic spike the rates provider starts refusing calls, because it received 40 identical requests in one second. Why, and what do you change?
- What it tests
- Why GetOrAdd's factory can run several times for one key, how to load each key once, what a failed load leaves behind, and keeping side effects out of factories.
- First step
- Note that the factory runs outside the dictionary's locks: every request that misses the key before the first load is stored runs LoadRates. Count the requests that arrive within one load's time.
- Follow-ups
-
- Can your fix still create extra objects under contention?
- The load sometimes fails. What does the cache do then?
- How do you refresh the rates every minute without a burst of loads?
From the lesson Concurrent collections and channels
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
A service writes telemetry events into Channel.CreateUnbounded and a background consumer posts them in batches to a collector. During a two-hour collector outage the process grew by 3 GB and was restarted, losing every queued event. What would you change?
- What it tests
- Back-pressure with bounded channels, choosing a full mode by what the data is worth, sizing the bound, and making the loss visible.
- First step
- Work out the backlog: the producer's rate times the outage. An unbounded channel keeps all of it in memory, so decide how much may wait, and what happens to the rest once that is full.
- Follow-ups
-
- Which full mode and which capacity would you choose?
- How do you know how many events were dropped?
- Some events, such as security audit records, must never be dropped. What then?
From the lesson Concurrent collections and channels
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A log pipeline of a reader, four parsers and an aggregator, joined by channels, sometimes reports counts that are too low, and nothing is logged. A malformed line was found in the input. Explain how the error disappeared, and how errors should travel through such a pipeline.
- What it tests
- How an error travels between the stages of a channel pipeline, observing faulted tasks, who completes a channel that several stages write to, and stopping the other stages when one fails.
- First step
- Follow the malformed line: the parser that reads it throws and its task faults. Then ask who awaits that task, and how the channel after the parsers gets completed.
- Follow-ups
-
- Which stage completes the channel that four parsers write to?
- How do the other stages stop once one has failed?
- Would you skip bad lines instead of failing the run?
From the lesson Concurrent collections and channels
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Your service calls a partner API that allows 100 requests a minute for each API key, from 4 instances. You added SemaphoreSlim(10) around the calls and still get 429 Too Many Requests. Why, and what do you build?
- What it tests
- Telling a concurrency limit from a rate limit, limits per instance against a shared budget, and handling a 429 with its Retry-After hint.
- First step
- Work out what the semaphore allows: 10 calls at once of about 200 ms each is about 3,000 calls a minute from one instance. The partner counts starts per minute, which the semaphore never counts.
- Follow-ups
-
- How do you share the budget across the four instances?
- What do you do with a 429 that still arrives?
- The partner also allows only 5 connections at once. What changes?
From the lesson Throttling with SemaphoreSlim and rate limiters
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
After an outage of a payment provider, the workers of an order service stop processing. A dump shows every worker awaiting SemaphoreSlim.WaitAsync, and the provider has been healthy for an hour. What happened, and how do you make sure it cannot recur unnoticed?
- What it tests
- Places leaked on failure paths, Release in finally, time limits on waits, releases that come once too often, and watching CurrentCount.
- First step
- Look at every path between WaitAsync and Release. A Release that a thrown exception skips leaks one place per failure, and an outage produces many failures in a row.
- Follow-ups
-
- How do you find the leaking path in the code?
- How do you get the service working again without a restart?
- What would you monitor from now on?
From the lesson Throttling with SemaphoreSlim and rate limiters
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Senior to staff
Design a per-user limit of 10 requests a minute with bursts of up to 3 for a public API, and explain how you would test its edges without waiting minutes in every test.
- What it tests
- Token bucket design per key, partitioned limiters, retry-after answers, testing time through an injected clock, and what a limit means across instances.
- First step
- Pick the algorithm from the requirement: a burst of 3 and an average of 10 a minute is a token bucket of 3 tokens with one token back every 6 s, kept for each user.
- Follow-ups
-
- What does a rejected request get back?
- Why not set AutoReplenishment to false and call TryReplenish in the tests?
- The API runs on 3 instances. Is the limit still 10 a minute?
From the lesson Throttling with SemaphoreSlim and rate limiters
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A developer replaced a for loop over 2,000 orders with Parallel.For, and the endpoint got slower while its processor use doubled. When does this happen, and how do you decide whether to parallelise a loop?
- What it tests
- The fixed cost of a parallel loop against the work per item, measuring before and after, and the parallelism a server already gets from its requests.
- First step
- Estimate the work per order against the cost of handing out iterations, then ask what else the server's processors are doing while this one request runs.
- Follow-ups
-
- How would you measure the two versions fairly?
- When does a parallel loop inside a request make sense?
- What would you try before Parallel.For?
From the lesson Parallel.For, Parallel.ForEach and PLINQ
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A CPU-bound batch job runs 6 times faster on 16 cores than on 1, and adding cores barely helps. Explain what limits it, and what you would measure to raise the speed-up.
- What it tests
- Amdahl's law with numbers, the serial part, uneven work and partitioning, and contention for shared state or memory.
- First step
- Fit Amdahl's law to the measurement: work out what share of the job must behave as serial for 16 cores to give only 6 times one core, and what ceiling that share sets however many cores are added.
- Follow-ups
-
- How do you find the serial part?
- How can uneven work per item limit the speed-up?
- Is a speed-up of 6 on 16 cores a bad result?
From the lesson Parallel.For, Parallel.ForEach and PLINQ
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A nightly job collects results with results.Add(x) inside Parallel.ForEach. Its totals are off by a few hundred on some nights, and about once a week it fails with an IndexOutOfRangeException. Explain both and fix them.
- What it tests
- Why List<T> is not safe for concurrent adds, the pattern of a collection per thread, concurrent collections, and testing parallel code.
- First step
- Write down, step by step, what List<T>.Add has to do to store one item, then ask what happens when two threads run those steps on the same list at the same moment, including the moment the list grows.
- Follow-ups
-
- Which fix keeps the order of the input?
- Would a lock around Add be acceptable?
- How do you make the bug show up in a test?
From the lesson Parallel.For, Parallel.ForEach and PLINQ
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
All C# & .NET (C# 14, .NET 10 LTS) interview questions · The C# & .NET (C# 14, .NET 10 LTS) track