Skip to content

Absolutely. Let’s treat this as a real engineering topic, not a classroom one.


CancellationToken and long-running operations in .NET

In normal business apps, cancellation is already important.

In industrial desktop systems, it becomes a survival feature.

Because here, “stop” is not just a UI action. It can mean:

  • stop a machine that is moving
  • stop image acquisition in the middle of a run
  • stop a multi-step inspection workflow without leaving hardware in a bad state
  • stop background processing during app shutdown without corrupting data
  • stop a live stream of results without freezing the UI

That is why cancellation in .NET is not just about “ending a task.” It is really about coordinated, safe interruption of work.

And that is also why CancellationToken is built around cooperative cancellation, not violent termination.


PART 1 — BIG PICTURE

Why cancellation is critical in real systems

In a toy app, cancellation is mostly about user experience.

In a real industrial system, cancellation is about:

  • safety
  • correctness
  • resource cleanup
  • system stability
  • operator trust

Imagine a wafer inspection machine running a recipe.

The operator presses Stop because:

  • the wrong wafer was loaded
  • the recipe is incorrect
  • the machine is behaving unexpectedly
  • they need to switch jobs immediately
  • an emergency condition occurred upstream

If the software cannot stop properly, several bad things can happen:

  • the stage keeps moving
  • the camera remains locked or streaming
  • the UI says “stopped” but hardware is still busy
  • partial results get saved as if inspection completed normally
  • the next job starts while the previous one has not fully released resources

In industrial systems, that is how you get unstable software, broken workflows, and angry operators.

Cancellation is critical because long-running work is everywhere:

  • machine initialization
  • homing and stage movement
  • autofocus
  • image capture
  • inspection pipelines
  • result uploads
  • defect classification
  • live event streaming
  • recipe execution
  • background watchdog loops

These operations are often multi-step, stateful, and touch real resources.

So cancellation must be treated as part of the workflow design, not an afterthought.


Why “just stop the task” is not simple

A lot of engineers come in with a mental model like this:

“I started some async work. If I want it to stop, I should be able to kill it.”

That sounds simple, but in real systems it is dangerous.

Because a running operation may currently be:

  • updating shared state
  • waiting on hardware
  • writing a file
  • holding a lock
  • halfway through a transaction
  • controlling a physical device
  • inside unmanaged driver code
  • in the middle of a sequence that must be completed or rolled back safely

If you “hard stop” at an arbitrary instruction, you do not know what state you leave behind.

That is the real issue.

The difficulty is not stopping computation. The difficulty is stopping it without breaking invariants.

For example:

A wafer inspection sequence may be:

  1. move stage to X/Y
  2. turn on illumination
  3. trigger camera
  4. capture image
  5. save raw frame
  6. run defect detection
  7. publish result
  8. move to next position

Now suppose cancellation happens between 4 and 5.

Questions immediately appear:

  • Do we discard that frame?
  • Do we save it as partial?
  • Is the stage still moving?
  • Is the light still on?
  • Is the camera still armed?
  • Does the workflow state say “Canceled” or “Failed”?
  • Can the next run start immediately?

That is why “stop” is a workflow design problem, not only a threading problem.


Why cooperative cancellation exists

This is the core idea.

.NET does not generally try to forcibly kill arbitrary running operations.

Instead, it gives you a signal:

“Cancellation has been requested. Please stop when it is safe to stop.”

That is cooperative cancellation.

This model exists because the code doing the work is the code that understands:

  • where safe stopping points are
  • what cleanup is required
  • what partial state must be rolled back or finalized
  • which resources must be released
  • which hardware commands must still complete

That makes it much more suitable for production systems.

For example, when stopping an inspection mid-run, good code may do this:

  • stop accepting new wafer positions
  • let the current camera trigger finish or abort in a driver-specific way
  • move stage to safe position
  • turn off illumination
  • flush result buffers
  • mark run as canceled
  • release hardware handles
  • notify UI and logs

That is cooperative cancellation in practice.

Not “kill now.” But “stop safely and consistently.”


PART 2 — HOW IT ACTUALLY WORKS

What CancellationToken is

CancellationToken is basically a lightweight signal object that says:

  • cancellation has not been requested yet
  • or cancellation has been requested

It does not stop code by itself.

This is the most important point.

A token is not a killer. It is a shared cancellation signal.

The code must actively cooperate by:

  • checking whether cancellation was requested
  • passing the token into cancelable async APIs
  • throwing or returning early when cancellation is observed
  • cleaning up properly

Usually, a token comes from a CancellationTokenSource.

Example:

csharp
using var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

// later
cts.Cancel();

When Cancel() is called, the token changes state to canceled, and code that is observing that token can react.


How cancellation is signaled

There are a few common ways code reacts to cancellation.

1. Polling the token

Code checks:

csharp
if (token.IsCancellationRequested)
{
    // stop work
}

This is useful in loops or multi-step workflows.

2. Throwing via ThrowIfCancellationRequested

csharp
token.ThrowIfCancellationRequested();

This throws OperationCanceledException, which is the normal .NET way to indicate cancellation.

That matters because cancellation is not usually treated as a normal failure. It is a controlled interruption.

3. Passing the token into async APIs

Many .NET APIs accept a token:

csharp
await Task.Delay(1000, token);
await httpClient.SendAsync(request, token);
await channelReader.ReadAsync(token);

In these cases, the API itself cooperates. If cancellation is requested, it wakes up and stops appropriately.

4. Registering callbacks

Less common, but possible:

csharp
token.Register(() => Console.WriteLine("Canceled"));

This can be useful for triggering some cleanup or notification, though in production code you should be careful not to put too much logic into token callbacks.


Cooperative cancellation model

The model is simple:

  • one piece of code requests cancellation
  • another piece of code observes it
  • the work stops at a safe point
  • cleanup happens
  • the operation exits in a predictable way

That sounds clean, but real systems add complexity.

Because not all operations are equally cancelable.

There are roughly three categories:

Fully cancelable work

Work that naturally supports cancellation well.

Examples:

  • Task.Delay
  • async streams waiting for data
  • network requests with token support
  • loops that check token frequently

Partially cancelable work

Work where some parts can stop quickly, but others cannot.

Examples:

  • a multi-step inspection workflow
  • file processing where current item finishes first
  • stage motion where stop command must be sent and acknowledged

Poorly cancelable work

Work that does not respond well to tokens.

Examples:

  • blocking driver calls with no timeout or cancel support
  • legacy synchronous APIs
  • unmanaged vendor SDKs that just block until done
  • CPU-heavy tight loops that never check token

This distinction is very important in industrial systems.

A common mistake is assuming that because a method accepts CancellationToken, the whole operation is truly cancelable. Often it is not. Sometimes only part of it is.

A senior engineer always asks:

  • where are the actual cancellation boundaries?
  • how quickly can this stop?
  • what happens if cancellation comes at the worst possible moment?
  • what must still run after cancellation?

PART 3 — REAL PROBLEMS IN THIS SYSTEM

Let’s use your example:

A WPF desktop app controlling a wafer inspection machine

This is exactly the kind of system where cancellation becomes subtle.


Stopping inspection safely

Suppose the machine is scanning wafer positions and capturing images.

A naive engineer may think:

“When Stop is clicked, cancel the token and end the async method.”

But a real stop usually needs a sequence.

For example:

  1. mark workflow as stopping
  2. prevent new movement commands from being queued
  3. stop current image acquisition if driver supports it
  4. stop or park stage motion safely
  5. turn off light source
  6. flush in-memory buffers if needed
  7. persist run status as canceled
  8. release hardware session / handles
  9. transition UI from Running to Canceled

This is important: canceling the workflow is not the same as instantly stopping the machine.

Often you need a dedicated safe-stop procedure.

That means the token may trigger a stop sequence, but the actual physical stop is still explicit domain logic.


Canceling machine calls

This is where theory meets ugly reality.

Many machine SDKs are:

  • synchronous
  • blocking
  • COM-based
  • unmanaged
  • inconsistent in timeout behavior
  • not designed around .NET cancellation

So code like this may not work the way people hope:

csharp
await Task.Run(() => machine.MoveTo(position), token);

If the token is canceled after the task starts, the token does not magically interrupt machine.MoveTo(position).

It only cancels before scheduling, or it cancels the wrapper task’s logical state. The underlying call may still be running.

This is one of the biggest real-world misunderstandings.

If the hardware SDK does not support cancellation, then you often need one of these patterns:

  • use a driver-specific stop/abort command
  • isolate the blocking call on a dedicated worker thread
  • use timeouts plus recovery logic
  • structure the command layer as a state machine
  • serialize hardware access so cancellation does not create overlapping commands

In other words, cancellation at the .NET layer is only part of the story. The hardware layer must support some kind of safe interruption too.


Partially completed workflows

A long-running inspection workflow is rarely one atomic thing.

It often consists of many steps:

  • validate recipe
  • initialize machine state
  • home axes
  • move to start position
  • autofocus
  • capture
  • analyze
  • save results
  • notify operator
  • move to next site

If cancellation happens halfway, you now have a partially completed run.

That creates questions like:

  • Do we keep partial data?
  • Do we discard incomplete site results?
  • Do we resume later?
  • How do we display canceled status in the UI?
  • Do downstream consumers understand “canceled” versus “failed”?
  • Can the machine immediately accept a new run?

This is why cancellation must be modeled in the domain.

A mature system has explicit states such as:

  • Running
  • Stopping
  • Canceled
  • Failed
  • Completed

Not just boolean flags.

Because operators, logs, metrics, and recovery logic need to distinguish these outcomes.


Cleaning up resources

In these systems, resource cleanup is everything.

Resources can include:

  • camera sessions
  • frame grabbers
  • serial ports
  • sockets
  • stage controller connections
  • file streams
  • memory-mapped buffers
  • GPU resources
  • background event subscriptions
  • timers

When cancellation happens, these must be cleaned up reliably.

The scary failures in production are often not immediate crashes. They are dirty leftovers:

  • camera remains locked by previous run
  • file handle still open, next run cannot write
  • event callback still firing into disposed UI
  • stage controller still thinks motion command is active
  • acquisition thread still consumes memory in background

This is why finally blocks matter so much in cancelable workflows.

Cancellation is not a special exemption from cleanup. If anything, it demands even more cleanup discipline.


PART 4 — HOW WE USE IT IN .NET (PRACTICAL)

Now let’s look at practical patterns.


Passing CancellationToken through layers

One of the most important habits in .NET is this:

If a method performs long-running work, it should usually accept a CancellationToken.

That token should be passed through layers, not swallowed.

Example structure:

  • UI layer starts inspection with a token
  • application/service layer passes token down
  • workflow layer passes token down
  • hardware/service APIs pass token down where possible

Example:

csharp
public async Task StartInspectionAsync(Recipe recipe, CancellationToken cancellationToken)
{
    await _inspectionWorkflow.RunAsync(recipe, cancellationToken);
}

Then:

csharp
public async Task RunAsync(Recipe recipe, CancellationToken cancellationToken)
{
    await _machine.InitializeAsync(cancellationToken);
    await _scanner.ScanWaferAsync(recipe, cancellationToken);
    await _resultPublisher.FlushAsync(cancellationToken);
}

Then:

csharp
public async Task ScanWaferAsync(Recipe recipe, CancellationToken cancellationToken)
{
    foreach (var site in recipe.Sites)
    {
        cancellationToken.ThrowIfCancellationRequested();

        await _stage.MoveToAsync(site.Position, cancellationToken);
        await _camera.CaptureAsync(cancellationToken);
        await _processor.ProcessAsync(site, cancellationToken);
    }
}

This is basic, but it is the right foundation.

The anti-pattern is when the UI has a token, but the deeper layers ignore it completely.


Canceling async workflows

A realistic inspection runner might look like this:

csharp
public async Task RunInspectionAsync(Recipe recipe, CancellationToken cancellationToken)
{
    _stateMachine.TransitionTo(InspectionState.Running);

    try
    {
        await _machineController.PrepareAsync(cancellationToken);

        foreach (var site in recipe.Sites)
        {
            cancellationToken.ThrowIfCancellationRequested();

            await _machineController.MoveToSiteAsync(site, cancellationToken);
            var image = await _cameraController.CaptureImageAsync(cancellationToken);
            var defects = await _inspectionEngine.AnalyzeAsync(image, cancellationToken);

            await _resultStore.SaveSiteResultAsync(site, defects, cancellationToken);
            await _eventBus.PublishSiteCompletedAsync(site.Id, cancellationToken);
        }

        _stateMachine.TransitionTo(InspectionState.Completed);
    }
    catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
    {
        _stateMachine.TransitionTo(InspectionState.Stopping);

        await _machineController.SafeStopAsync(CancellationToken.None);

        _stateMachine.TransitionTo(InspectionState.Canceled);
        throw;
    }
    catch (Exception ex)
    {
        _stateMachine.TransitionTo(InspectionState.Failed);
        _logger.LogError(ex, "Inspection failed.");
        throw;
    }
    finally
    {
        await _cameraController.ReleaseAsync(CancellationToken.None);
        await _machineController.ReleaseAsync(CancellationToken.None);
    }
}

There are several important production ideas here.

First, cancellation is caught separately from failure.

Second, cleanup and safe-stop may intentionally use CancellationToken.None.

Why?

Because once cancellation has been requested, some cleanup still must happen. You do not want cleanup itself to be skipped because the original token is already canceled.

This is a very important real-world pattern.

Use the request token to cancel optional work. Do not use it blindly for mandatory cleanup.


Cancellation in loops and event streaming

Industrial systems often have loops that continuously read events or stream results.

For example, a background event loop reading machine status:

csharp
public async Task ReadMachineEventsAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        var evt = await _machineEventSource.ReadNextAsync(cancellationToken);
        await _uiEventDispatcher.PublishAsync(evt, cancellationToken);
    }
}

This is good when the underlying API supports token-aware waiting.

For polling systems:

csharp
public async Task MonitorTemperatureAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        var temperature = _sensor.ReadTemperature();
        _logger.LogDebug("Current temperature: {Temperature}", temperature);

        if (temperature > _limits.MaxTemperature)
        {
            _alarmService.RaiseOverheatAlarm(temperature);
        }

        await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
    }
}

A few real points here:

  • the loop checks cancellation every iteration
  • the delay is cancelable, so shutdown is responsive
  • polling interval is explicit and controlled

Without cancelable delay, these loops often become sluggish during shutdown.


Linked tokens: UI cancel + system shutdown

This is extremely useful.

In a WPF app, a workflow may need to stop if:

  • user clicked Cancel
  • application is shutting down
  • machine emergency stop occurred
  • parent operation failed

You can combine tokens using CancellationTokenSource.CreateLinkedTokenSource.

Example:

csharp
using var userCts = new CancellationTokenSource();
using var shutdownCts = new CancellationTokenSource();

using var linkedCts =
    CancellationTokenSource.CreateLinkedTokenSource(
        userCts.Token,
        shutdownCts.Token);

await _inspectionService.RunInspectionAsync(recipe, linkedCts.Token);

Now the inspection stops if either token is canceled.

This is very practical.

For example:

  • UI has a Cancel button using userCts.Cancel()
  • app shutdown handler uses shutdownCts.Cancel()

The workflow does not need separate logic paths everywhere. It just observes one token.


WPF example: cancel button + safe workflow

Here is a more realistic ViewModel pattern:

csharp
public sealed class InspectionViewModel
{
    private readonly IInspectionService _inspectionService;
    private CancellationTokenSource? _runCts;

    public bool IsRunning { get; private set; }

    public InspectionViewModel(IInspectionService inspectionService)
    {
        _inspectionService = inspectionService;
    }

    public async Task StartInspectionAsync(Recipe recipe, CancellationToken appShutdownToken)
    {
        if (IsRunning)
            return;

        _runCts = new CancellationTokenSource();

        using var linkedCts =
            CancellationTokenSource.CreateLinkedTokenSource(
                _runCts.Token,
                appShutdownToken);

        IsRunning = true;

        try
        {
            await _inspectionService.RunInspectionAsync(recipe, linkedCts.Token);
        }
        catch (OperationCanceledException) when (linkedCts.Token.IsCancellationRequested)
        {
            // show canceled state, not an error popup
        }
        catch (Exception ex)
        {
            // show actual failure
            Console.WriteLine(ex);
        }
        finally
        {
            IsRunning = false;
            _runCts.Dispose();
            _runCts = null;
        }
    }

    public void CancelInspection()
    {
        _runCts?.Cancel();
    }
}

This is a normal pattern:

  • ViewModel owns a CTS for current run
  • cancel button calls Cancel()
  • token is linked with app shutdown
  • cancellation is treated differently from failure

That last point matters a lot in UX.

Cancellation is usually operator intent, not a system error.


When hardware API is blocking and not token-aware

This is common enough to call out directly.

Suppose the vendor SDK exposes:

csharp
_camera.CaptureBlocking();

No token. No timeout. No async.

You cannot magically make this truly cancelable.

What you can do is wrap it carefully:

csharp
public async Task<ImageFrame> CaptureAsync(CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    return await Task.Run(() =>
    {
        return _camera.CaptureBlocking();
    }, cancellationToken);
}

But this only helps before the task starts or for task composition. It does not interrupt CaptureBlocking() once inside.

So the better real-world design is often:

  • command hardware on a dedicated thread or agent
  • expose a separate abort/stop command if vendor SDK supports it
  • use timeout and fault recovery
  • ensure only one hardware command runs at a time

For example:

csharp
public async Task<ImageFrame> CaptureAsync(CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    using var registration = cancellationToken.Register(() => _camera.TryAbortCapture());

    return await _hardwareScheduler.RunAsync(() => _camera.CaptureBlocking(), cancellationToken);
}

This is much closer to real production thinking.

The token is not interrupting the method by magic. It is triggering a hardware-specific abort path.

That is the key mindset.


PART 5 — COMMON MISTAKES (VERY REALISTIC)

Ignoring cancellation tokens

This is the most common one.

A method accepts CancellationToken, but never checks it and never passes it down.

Example:

csharp
public async Task ProcessAsync(CancellationToken cancellationToken)
{
    await Task.Delay(5000); // token ignored
}

Looks harmless. In production it means:

  • cancel button feels broken
  • shutdown takes too long
  • workflows continue after operator requested stop
  • UI and machine state diverge

In industrial systems, that breaks trust quickly.

Operators do not care that the code was “technically async.” They care whether Stop actually stops.


Not propagating tokens

Another common failure:

  • UI creates token
  • service layer forgets to pass it
  • repository or hardware layer uses CancellationToken.None
  • cancellation dies in the middle layers

This creates false confidence because the top-level API looks cancelable but the real work is not.

A senior engineer reviews long-running call chains specifically for token propagation.

Because cancellation is only as strong as the weakest layer.


Force-stopping threads

This is old-school dangerous thinking.

Trying to abort threads or kill work violently is almost always a bad sign.

Historically people looked for things like:

  • Thread.Abort
  • killing worker threads
  • unloading execution context abruptly

The problem is the same one we discussed earlier: you leave state inconsistent.

Production consequences:

  • hardware left in unknown state
  • locks not released predictably
  • resource corruption
  • application instability
  • nearly impossible debugging

In industrial systems, “forced stop” may exist only at the outermost safety level, and even then it is usually implemented through machine-specific emergency procedures, not random thread killing.


Not cleaning up resources after cancellation

This one is extremely real.

An engineer handles OperationCanceledException, logs “Canceled,” and returns.

But they forgot:

  • release camera
  • stop stage motion
  • dispose stream
  • unsubscribe event handler
  • release semaphore
  • update state machine
  • persist canceled status

Then the next run behaves strangely.

This produces the worst class of bugs: works once, fails on second run.

These are classic resource-cleanup bugs after cancellation.

That is why mature cancelable code nearly always has:

  • try
  • catch (OperationCanceledException)
  • finally

Not because it looks elegant, but because it survives real usage.


Treating cancellation as an error everywhere

A canceled operation is not always a failure.

If the operator pressed Stop, then cancellation is expected behavior.

If logs, alarms, telemetry, and UI treat cancellation exactly like an exception failure, you get noisy operations:

  • false alarms
  • misleading error dashboards
  • operators trained to ignore warnings
  • support engineers chasing fake incidents

Good systems distinguish clearly between:

  • canceled by user
  • canceled by shutdown
  • timed out
  • failed unexpectedly

These are operationally different events.


PART 6 — PERFORMANCE & TRADE-OFFS

Cost of checking cancellation

Checking token.IsCancellationRequested is cheap.

Calling ThrowIfCancellationRequested() is also cheap when not canceled.

So in most application code, the overhead is tiny.

The bigger performance question is not “can I afford to check?” It is:

  • how often should I check?
  • where are the safe interruption points?

For example, in a long CPU loop:

csharp
for (int i = 0; i < items.Length; i++)
{
    if ((i & 255) == 0)
        cancellationToken.ThrowIfCancellationRequested();

    Process(items[i]);
}

Checking every iteration may be fine. Sometimes checking every N iterations is enough.

The trade-off is:

  • frequent checks = more responsive cancellation
  • less frequent checks = slightly lower overhead, but slower stop

In most industrial apps, responsiveness matters more than micro-optimizing token checks.


Responsiveness vs overhead

This trade-off becomes more interesting in:

  • hot loops
  • high-frequency event processing
  • image analysis pipelines
  • real-time streaming paths

But even there, token checks are rarely the bottleneck. Usually the real cost lies in:

  • I/O
  • hardware calls
  • image processing
  • locking
  • marshaling between threads
  • UI updates

So the usual advice is:

  • check cancellation at meaningful boundaries
  • do not obsess over token-check cost
  • optimize only after measurement

A good practical rule:

  • check before starting expensive work
  • check between workflow steps
  • check inside long loops
  • pass token into all blocking/waiting APIs that support it

That gives good responsiveness without cluttering every line.


PART 7 — SENIOR ENGINEER THINKING

How experienced engineers design cancelable workflows

Senior engineers do not think of cancellation as a boolean. They think of it as a workflow contract.

They ask:

  • what does cancel mean in this domain?
  • what is allowed to finish?
  • what must stop immediately?
  • what cleanup is mandatory?
  • what state should be persisted?
  • can the operation be retried safely later?

They define explicit cancellation behavior per workflow.

For example, in a wafer inspection system:

  • no new site processing after cancellation request
  • current capture may finish or abort depending on device support
  • stage must move to safe position
  • camera and light must be released
  • partial results must be marked incomplete
  • run state must become Canceled, not Failed
  • next run cannot begin until safe-stop completes

That is design thinking, not API thinking.


How to guarantee safe stop

You rarely guarantee safe stop with token alone.

You guarantee it with layers of design:

1. Explicit workflow states

Use states like:

  • Idle
  • Starting
  • Running
  • Stopping
  • Canceled
  • Failed
  • Completed

This prevents confused transitions.

2. Controlled command ownership

Do not let random parts of the app command hardware directly.

Use a machine controller / workflow orchestrator / command queue so stop behavior is centralized.

3. Safe-stop procedures

Have explicit methods such as:

  • SafeStopAsync
  • AbortCaptureAsync
  • ParkStageAsync
  • ReleaseHardwareAsync

These are domain operations, not generic cancellation.

4. Mandatory cleanup paths

Use finally and dedicated cleanup logic that runs even after cancellation.

5. Timeouts and escalation

If graceful stop does not complete in time, escalate:

  • retry stop command
  • isolate subsystem
  • mark hardware faulted
  • require operator intervention

This is much better than pretending cancellation always works perfectly.


How to design idempotent cancellation

This is a very senior topic.

Idempotent cancellation means:

If stop is requested multiple times, or cleanup runs more than once, the system remains correct.

That matters because in real systems:

  • user may click Stop twice
  • shutdown may race with user cancel
  • multiple components may observe the same failure and all try to stop
  • cleanup may be retried after partial failure

Good cancelable design therefore makes stop operations safe to repeat.

For example:

csharp
public async Task SafeStopAsync(CancellationToken cancellationToken)
{
    if (_state == MachineState.Stopped || _state == MachineState.Idle)
        return;

    _state = MachineState.Stopping;

    try
    {
        await _stage.TryStopAsync(cancellationToken);
        await _light.TryTurnOffAsync(cancellationToken);
        await _camera.TryAbortAsync(cancellationToken);
    }
    finally
    {
        _state = MachineState.Stopped;
    }
}

The important idea is not the exact code. It is that calling stop twice should not make things worse.

Likewise, cleanup methods should ideally tolerate already-released resources.

Examples:

  • closing an already-closed stream should be harmless
  • unsubscribing an already-unsubscribed handler should be harmless
  • releasing already-idle hardware should be harmless

That is how you build systems that survive race conditions and operational noise.


Final mental model

For real industrial .NET systems, think about cancellation like this:

CancellationToken is not a kill switch.

It is a request to stop, and your software must translate that request into:

  • safe interruption points
  • domain-aware stop logic
  • reliable cleanup
  • correct state transitions
  • predictable operator behavior

The junior mindset is:

“How do I cancel this task?”

The senior mindset is:

“What does safe cancellation mean for this workflow, this hardware, and this system state?”

That is the difference between code that demos well and software that survives production.

If you want, next I can turn this into a technical leadership interview Q&A set for CancellationToken, with strong answers and follow-up questions.

Docs-first project memory for AI-assisted implementation.