No Thread Suspend Resume
ID |
csharp.no_thread_suspend_resume |
Severity |
critical |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
CSharp |
Tags |
concurrency, deadlock, obsolete-api, reliability |
Description
Reports calls to Thread.Suspend() and Thread.Resume(). Both have been obsolete since .NET
Framework 2.0: they stop a thread at an arbitrary instruction, so the thread can be frozen while
holding a lock — the classic unrecoverable deadlock.
Rationale
Suspend does not ask the target thread to stop at a safe point; it stops it wherever it happens to
be. That point may be inside a lock block, inside a static constructor, inside the allocator, or
in the middle of a framework call that owns an internal lock. Every other thread that needs that
lock then blocks indefinitely. There is no timeout, no exception, and no stack that points at the
Suspend call — a process dump is the only diagnostic left.
The pair is also useless as a synchronisation primitive, for two reasons. First, the caller has no
idea what state the target left behind, so "suspend, read shared data, resume" is guaranteed to
observe torn intermediate state some of the time. Second, the calls are not counted or ordered: a
Resume racing ahead of its Suspend is lost, and the thread stays suspended forever.
This is why the framework marked both methods obsolete and why .NET Core removed them: on modern
targets the call throws PlatformNotSupportedException at runtime. Any occurrence is therefore
either dead code on the current target or a latent deadlock on the old one.
using System;
using System.Threading;
public class Coordinator
{
private readonly ManualResetEventSlim gate = new ManualResetEventSlim(false);
public void Pause(Thread worker)
{
worker.Suspend(); // FLAW — worker may be frozen holding a lock
}
public void Continue(Thread worker)
{
worker.Resume(); // FLAW — races with Suspend, may be lost
}
public void WaitForWork()
{
gate.Wait(); // OK — the thread stops at a point it chose
}
public void ReleaseWork()
{
gate.Set(); // OK — cooperative signal, no lock can be stranded
}
}
Remediation
Replace suspension with cooperative signalling: the worker decides where it is safe to stop.
-
ManualResetEventSlimorSemaphoreSlim— the worker callsWait()at a safe point in its loop and the controller callsSet()/Release(). -
CancellationToken— when the goal is to stop the work rather than pause it, pollThrowIfCancellationRequested()at the same safe point. -
Monitor.Wait/Monitor.Pulse— when the pause is tied to a condition over shared state that is already guarded by a lock.
None of these can freeze a thread inside a lock, and all of them make the resume path explicit and idempotent.