Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

C# Threading Fundamentals: Mastering System.Threading.Thread

Tech 2

Instantiating and Launching Workers

Threads execute concurrently with the main application flow. By default, a newly created thread begins execution immediately upon calling Start(), allowing it to run in parallel with the primary process.

using static System.Console;
using static System.Threading.Thread;

namespace ConcurrentBasics
{
    public class Executor
    {
        public static void Main()
        {
            var worker = new Thread(SimulateWork);
            worker.Start();
            SimulateWork(); // Main runs alongside the spawned thread
            Console.ReadLine();
        }

        private static void SimulateWork()
        {
            WriteLine("Process initiated.");
            for (int n = 1; n <= 8; n++)
            {
                WriteLine(n);
                Sleep(100);
            }
        }
    }
}

Temporarily Halting Execution

The Thread.Sleep method pauses the executing thread for a specified duration. During this period, the OS scheduler reallocates CPU time to other ready threads, preventing busy-waiting and conserving system resources. Note that sleep order does not guarantee execution order due to asynchronous scheduling.

public class DelayedRunner
{
    public static void Main()
    {
        var delayedJob = new Thread(LegacyRoutine);
        delayedJob.Start();
        CurrentRoutine();
    }

    private static void CurrentRoutine()
    {
        WriteLine("Rapid sequence started.");
        for (int i = 0; i < 5; i++)
            WriteLine($"Fast task: {i}");
    }

    private static void LegacyRoutine()
    {
        WriteLine("Slow sequence initiated.");
        for (int i = 0; i < 5; i++)
        {
            Sleep(TimeSpan.FromMilliseconds(200));
            WriteLine($"Delayed task: {i}");
        }
    }
}

Blocking Until Completion

To synchronize parent and child execution flows, invoke Join(). This forces the calling thread to block until the target thread finishes its assigned task, ensuring sequential dependency resolution.

public class SequentialWaiter
{
    public static void Main()
    {
        WriteLine("Host process launching...");
        var backgroundTask = new Thread(ElongatedCompute);
        backgroundTask.Start();
        backgroundTask.Join(); // Blocks here until compute finishes
        WriteLine("Calculation finished. Resuming host.");
    }

    private static void ElongatedCompute()
    {
        for (int k = 0; k < 4; k++)
        {
            Sleep(TimeSpan.FromSeconds(0.5));
            WriteLine(k);
        }
    }
}

Abrupt Termination & Modern Alternatives

Calling Abort() injects a ThreadAbortException into the target thread, attempting to force termination. This approach is strongly discouraged because exceptions can be caught via ResetAbort(), leading to unpredictable states or resource leaks. Prefer cooperative cancellation using CancellationTokenSource for reliable lifecycle management.

public class UnsafeTerminator
{
    public static void Main()
    {
        var volatileWorker = new Thread(RiskyOperation);
        volatileWorker.Start();
        Sleep(TimeSpan.FromSeconds(2));
        
        // WARNING: Abort throws ThreadAbortException, making cleanup unpredictable.
        volatileWorker.Abort(); 
        WriteLine("Forced termination triggered.");
    }

    private static void RiskyOperation()
    {
        try
        {
            for (int step = 0; step < 10; step++)
            {
                Sleep(TimeSpan.FromSeconds(1));
                WriteLine(step);
            }
        }
        catch (ThreadAbortException)
        {
            // ResetAbort() can cancel the abort, complicating control flow.
            Thread.ResetAbort(); 
        }
    }
}

Inspecting Thread Lifecycle States

The ThreadState enumeration tracks runtime conditions such as Unstarted, Running, WaitSleepJoin, and Stopped. Querying this property allows developers to monitor progress, diagnose hung processes, or implement state-dependent logic.

public class StateInspector
{
    public static void Main()
    {
        var targetThread = new Thread(MonitorableRoutine);
        var idleThread = new Thread(SuspendedState);

        WriteLine($"Target init: {targetThread.ThreadState}");
        WriteLine($"Idle init: {idleThread.ThreadState}");

        idleThread.Start();
        targetThread.Start();

        for (int cycle = 0; cycle < 10; cycle++)
        {
            WriteLine($"Live state: {targetThread.ThreadState}");
            Sleep(TimeSpan.FromMilliseconds(150));
        }

        targetThread.Interrupt(); // Triggers WaitSleepJoin -> Running -> Terminating
        WriteLine($"Post-interrupt: {targetThread.ThreadState}");
    }

    private static void MonitorableRoutine()
    {
        for (int tick = 0; tick < 8; tick++)
            Sleep(TimeSpan.FromMilliseconds(100));
    }

    private static void SuspendedState()
    {
        Sleep(TimeSpan.FromSeconds(5));
    }
}

Managing Priority & Core Affinity

Thread priority influences the OS scheduler's allocation of CPU cycles. While modern systems dynamically adjust weights, explicit priorities (Highest to Lowest) are useful in controlled environments. The ProcessorAffinity property restricts execution to specific logical cores, which can simulate load contention for testing.

public class PriorityAnalyzer
{
    public static void Main()
    {
        WriteLine($"Base priority: {CurrentThread.Priority}");
        RunStarvationTest();

        IntPtr mask = (IntPtr)1; // Force single core
        CurrentProcess().ProcessorAffinity = mask;
        WriteLine("Restricted to core zero.");
        RunStarvationTest();
    }

    private static void RunStarvationTest()
    {
        var scheduler = new LoadBalancer();
        var master = new Thread(scheduler.BurnCPU) { Name = "Alpha", Priority = ThreadPriority.Highest };
        var junior = new Thread(scheduler.BurnCPU) { Name = "Beta", Priority = ThreadPriority.Lowest };

        master.Start();
        junior.Start();

        Sleep(TimeSpan.FromSeconds(2));
        scheduler.Terminate();
    }
}

public class LoadBalancer
{
    private volatile bool _running = true;
    private long _ticks = 0;

    public void BurnCPU()
    {
        while (_running) _ticks++;
        WriteLine($"{CurrentThread.Name} ({CurrentThread.Priority}): {_ticks:N0} ops/sec");
    }

    public void Terminate() => _running = false;
}

Daemon vs Non-Daemon Lifecycles

Foreground threads prevent application shutdown until they complete. Conversely, marking a thread as IsBackground = true converts it to a daemon; if all foreground threads exit, the CLR terminates background workers instantly without waiting.

public class LifecycleController
{
    public static void Main()
    {
        var persistent = new TaskRunner(5);
        var ephemeral = new TaskRunner(10);

        var keeper = new Thread(persistent.Execute) { Name = "Primary" };
        var daemon = new Thread(ephemeral.Execute) { Name = "Secondary", IsBackground = true };

        keeper.Start();
        daemon.Start();

        keeper.Join();
        // Process ends immediately after Join because only Background remains
    }
}

public class TaskRunner
{
    private readonly int _limit;
    public TaskRunner(int steps) => _limit = steps;

    public void Execute()
    {
        for (int x = 0; x < _limit; x++)
        {
            Sleep(TimeSpan.FromMilliseconds(200));
            WriteLine($"{CurrentThread.Name}: Step {x}");
        }
    }
}

Injecting Context into Workers

Data transfer to isolated execution units requires careful scoping. Three standard patterns exist:

  1. Encapsulate parameters within a class instance passed via constructor.
  2. Pass data directly through Thread.Start(object).
  3. Utilize closure lambdas, bearing in mind captured variables are referenced by address, causing potential race conditions if modified externally.
public class DataInjections
{
    public static void Main()
    {
        var container = new PayloadStore(3);
        var workerA = new Thread(container.Process); workerA.Start(); workerA.Join();

        var workerB = new Thread(BatchProcessor); workerB.Start(4); workerB.Join();

        int dynamicVal = 5;
        var workerC = new Thread(() => Compute(dynamicVal));
        workerC.Start(); workerC.Join();

        int sharedScope = 10;
        var job1 = new Thread(() => Compute(sharedScope));
        sharedScope = 20; // Modifies what both see!
        var job2 = new Thread(() => Compute(sharedScope));
        job1.Start(); job2.Start();
    }

    private static void BatchProcessor(object limitObj)
    {
        for (int n = 0; n < (int)limitObj; n++)
            WriteLine($"Batch: {n}");
    }

    private static void Compute(int bound)
    {
        for (int v = 0; v < bound; v++)
            WriteLine($"Computed value: {v}");
    }
}

public class PayloadStore
{
    private readonly int _capacity;
    public PayloadStore(int size) => _capacity = size;
    public void Process()
    {
        for (int i = 0; i < _capacity; i++)
            WriteLine($"Stored payload index: {i}");
    }
}

Mutual Exclusion & Race Conditions

Concurrent access to shared mutable state causes data corruption. Increment/decrement operations on a shared integer demonstrate non-deterministic outcomes due to interleaved read-modify-write cycles. Applying lock ensures atomic execution phases, serializing access and preserving mathematical consistency.

public class ConcurrencyControl
{
    public static void Main()
    {
        var rawCounter = new MutableState();
        var protectedCounter = new SyncedState();

        RunWorkers(rawCounter, () => WriteLine($"Unprotected result: {rawCounter.Value}"));
        RunWorkers(protectedCounter, () => WriteLine($"Lock-protected result: {protectedCounter.Value}"));
    }

    private static void RunWorkers(IStateManager state, Action report)
    {
        var t1 = new Thread(() => Operate(state));
        var t2 = new Thread(() => Operate(state));
        t1.Start(); t2.Start();
        t1.Join(); t2.Join();
        report();
    }

    private static void Operate(IStateManager mgr)
    {
        for (int i = 0; i < 5000; i++)
        {
            mgr.Bump();
            mgr.Reduce();
        }
    }
}

public interface IStateManager
{
    int Value { get; }
    void Bump();
    void Reduce();
}

public class MutableState : IStateManager
{
    public int Value { get; private set; }
    public void Bump() => Value++;
    public void Reduce() => Value--;
}

public class SyncedState : IStateManager
{
    private readonly object _gate = new();
    public int Value { get; private set; }
    public void Bump() { lock (_gate) Value++; }
    public void Reduce() { lock (_gate) Value--; }
}

Preventing Circular Dependencies

Deadlocks occur when two or more threads wait indefinitely for resources held by eachother. The Monitor.TryEnter method mitigates this by accepting a timeout parameter. If the lock cannot be acquired within the specified window, the operation returns false, allowing graceful fallback instead of permanent suspension.

public class DeadlockMitigation
{
    public static void Main()
    {
        var nodeA = new object();
        var nodeB = new object();

        new Thread(() => ClaimResources(nodeA, nodeB)).Start();

        lock (nodeB)
        {
            Sleep(1000);
            if (Monitor.TryEnter(nodeA, TimeSpan.FromSeconds(3)))
            {
                WriteLine("Resourced acquired safely.");
                Monitor.Exit(nodeA);
            }
            else
            {
                WriteLine("Fallback triggered due to contention timeout.");
            }
        }

        WriteLine("--- Hazard Zone ---");
        new Thread(() => ClaimResources(nodeA, nodeB)).Start();
        lock (nodeB)
        {
            Sleep(1000);
            lock (nodeA) 
            {
                WriteLine("This section is unreachable in a deadlock scenario.");
            }
        }
    }

    private static void ClaimResources(object left, object right)
    {
        lock (left)
        {
            Sleep(1000);
            lock (right);
        }
    }
}

Containing Exceptions Within Execution Units

Exceptions raised inside a Thread do not propagate across thread boundaries. A try/catch block wrapping Thread.Start will never capture failures originating from the worker. All error handling must be implemented internally within the delegated method to prevent unhandled crashes and application termination.

public class ErrorBoundary
{
    public static void Main()
    {
        var isolatedErrorHandler = new Thread(SafeExecution);
        isolatedErrorHandler.Start();
        isolatedErrorHandler.Join();

        try
        {
            var unhandledError = new Thread(UnsafeExecution);
            unhandledError.Start();
        }
        catch (Exception ex)
        {
            // This block never executes. Thread exceptions bypass main thread scopes.
            WriteLine($"Caught: {ex.Message}"); 
        }
    }

    private static void SafeExecution()
    {
        try
        {
            for (int s = 0; s < 3; s++) Sleep(TimeSpan.FromMilliseconds(100));
            throw new InvalidOperationException("Internal failure simulated.");
        }
        catch (InvalidOperationException)
        {
            WriteLine("Failure contained within worker scope.");
        }
    }

    private static void UnsafeExecution()
    {
        Sleep(TimeSpan.FromMilliseconds(200));
        throw new InvalidProgramException("Unhandled worker crash.");
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.