CodeGym /Courses /C# SELF /Thread lifecycle and how to control it

Thread lifecycle and how to control it

C# SELF
Level 55 , Lesson 2
Available

1. Introduction

Imagine a thread as a tireless coworker you assign work to. The coworker can be sleeping (hasn't started yet), working hard (your method is executing), waiting for you to give a new task (idle), or finished (completed).

In C# (and .NET in general) a thread's lifecycle consists of several states:

  • Unstarted — the thread object is created but not yet started.
  • Running — the thread is executing.
  • WaitSleepJoin — the thread is temporarily inactive (for example, waiting for a signal or "sleeping").
  • Stopped — the thread finished its task and has terminated.

You can visualize this lifecycle with a diagram like this:

stateDiagram-v2
    [*] --> Unstarted
    Unstarted --> Running: Start()
    Running --> WaitSleepJoin: Wait/Sleep/Join
    WaitSleepJoin --> Running: Signal received/Timeout
    Running --> Stopped: Method finished
    WaitSleepJoin --> Stopped: Method finished
    Stopped --> [*]

Everything starts with creating a Thread object, but until you call Start(), the thread "dozes" in Unstarted. After Start() — the fun begins, the thread moves to Running. If the thread calls Thread.Sleep or waits for something (for example, Monitor.Wait), it goes into a special waiting state. As soon as the method given to the thread completes, the thread dies, ceases to exist and won't be "resurrected". It's a one-way ticket.

2. Practice: lifecycle of a simple thread

Let's look at a classic example:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        // Create the thread — for now we're just scheduling work
        Thread worker = new Thread(DoWork);

        Console.WriteLine($"Thread state after creation: {worker.ThreadState}");

        // Start the thread
        worker.Start();
        Console.WriteLine($"Thread state after start: {worker.ThreadState}");

        // Let the main thread sleep a bit so the worker has time to run
        Thread.Sleep(100);

        Console.WriteLine($"Thread state (later): {worker.ThreadState}");

        // Wait for the worker to finish (join)
        worker.Join();

        Console.WriteLine($"Thread state after completion: {worker.ThreadState}");
        Console.WriteLine("Main thread finished");
    }

    static void DoWork()
    {
        Console.WriteLine("Worker thread started working!");
        Thread.Sleep(500);
        Console.WriteLine("Worker thread finished working!");
    }
}

What does the program print?

  1. After creating the thread — the state will be Unstarted.
  2. After start — usually immediately Running (but it can be Running | Background).
  3. While working — the state can be Running, or WaitSleepJoin if the thread is "sleeping".
  4. After the method finishes — the state becomes Stopped.

This code is a great tool for understanding what state your thread can be in. Play with delays and see how the state changes.

3. Controlling a thread: main methods

Start: Start()

It's obvious but worth repeating: create a thread — start it with Start(). And you can only start it once: trying to call Start() again will throw ThreadStateException.

Thread t = new Thread(MyMethod);
t.Start();   // OK
t.Start();   // Error!

Waiting for completion: Join()

Sometimes you need to wait until a thread finishes before continuing. For that there's Join().

Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // Blocks the current thread until t completes

If you have multiple threads, you can call Join() for each — the main thread will wait until all workers finish.

Variants: there's an overload Join(int millisecondsTimeout) that waits only the specified time and then continues.

// Wait no more than 2 seconds
if (t.Join(2000))
    Console.WriteLine("Thread finished on time");
else
    Console.WriteLine("Got bored of waiting...");

Forceful stop: why it's a bad idea

In old .NET versions there was Thread.Abort(), which allowed killing a thread mid-flight. You rarely see it now — it's dangerous and can leave the program in a weird state. .NET philosophy is: a thread should finish voluntarily. You don't "kill" the coworker — you politely hint that the workday is over.

4. How to properly "stop" a thread

The most correct and safe way to stop a thread is to use a cancellation flag or a termination indicator that the thread checks periodically.

class Worker
{
    private volatile bool shouldStop = false;

    public void DoWork()
    {
        while (!shouldStop)
        {
            Console.WriteLine("Working!");
            Thread.Sleep(300);
        }

        Console.WriteLine("Thread is stopping on command.");
    }

    public void RequestStop()
    {
        shouldStop = true;
    }
}

Usage:

Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();

// Wait a bit
Thread.Sleep(1000);

// Ask the thread to finish
w.RequestStop();
t.Join(); // Wait for the thread to finish

Important note: volatile

The volatile keyword tells the compiler and CPU: "Don't cache this field, always read the latest value!" That's important so the thread sees the fresh stop flag. Without this (or other synchronization techniques) the thread might never notice your changes.

5. Threads entering waiting and sleeping states

Sometimes a thread temporarily does no work — it's either waiting or sleeping.

Sleep: Thread.Sleep

When you want to give a thread a rest or throttle execution (for example, to avoid hogging the CPU), use Thread.Sleep(milliseconds).

// Thread sleeps for 2 seconds
Thread.Sleep(2000);

While sleeping the thread does no work.

Waiting / Join

When the main thread waits for a child to finish (Join), the main thread is "on pause". Similarly, if a thread waits for a resource (for example, via monitors or other synchronization primitives), it moves into a special waiting state.

6. Managing thread backgroundness

In .NET threads come in two flavors: foreground and background. The difference is simple:

  • If only background threads remain in the process, the process will exit automatically.
  • The main thread and all foreground threads must finish for the process to stop.

You can explicitly mark a thread as background:

Thread t = new Thread(SomeMethod);
t.IsBackground = true; // Marked as background
t.Start();

Practical example — Daemon vs. regular thread

Thread t = new Thread(() =>
{
    while (true)
    {
        Console.WriteLine("I'm a phantom (background), you can't stop me!");
        Thread.Sleep(500);
    }
});
t.IsBackground = true; // Make it background
t.Start();

Thread.Sleep(1200);
Console.WriteLine("Main thread is finishing");
// After Main finishes — the process dies, and our eternal thread disappears too

After Main finishes — the process terminates; background threads are stopped automatically.

7. Useful nuances

What not to do with threads

  • Don't "restart" a thread. A Thread object lives once: once its method finished — the thread is dead, and calling Start() again will throw.
  • Don't forcibly stop someone else's thread using Thread.Abort() or Thread.Suspend() — those are obsolete and dangerous.
  • Don't ignore thread completion. If a thread works with files or resources, release them properly before the thread ends.

Checking state and managing lifecycle

if (t.IsAlive)
{
    Console.WriteLine("Thread is still alive");
}
else
{
    Console.WriteLine("Thread has finished");
}

IsAlive is true while the thread executes its method; after completion it's false.

Lifecycle of a simple .NET thread

State How to get there What it means? How to leave
Unstarted
new Thread(...)
Thread created, not started Call Start()
Running
Start()
Thread is doing work Finish the method
WaitSleepJoin Sleep(), Join(), waiting Thread is temporarily inactive Waiting ends
Stopped Thread method finished Thread is "dead" Won't leave — the end

FAQ about thread lifecycle management

Question: Can I kill a thread on command?
Answer: No and you shouldn't — threads should manage their own shutdown. Use cancellation flags.

Question: Can I reuse a Thread object?
Answer: No. Create a new object for new work.

Question: What happens if the main thread finishes while a child thread keeps running?
Answer: If the child thread is background (IsBackground == true), the application will exit. If not — the process will live until all threads finish.

Question: How to correctly clean up resources if a thread finishes due to cancellation?
Answer: Use try...finally blocks inside the thread method so resources are released in any case.

8. Common mistakes and how to avoid them when working with threads

Mistake #1: reusing the same Thread object.
You can't start the same thread object more than once. After a thread finishes you can't restart it — that'll throw an exception.

Mistake #2: improper cleanup of external resources inside a thread.
If a thread works with files, network or other resources, ensure they are closed and released properly. Prefer finally blocks or using constructs to avoid leaks and deadlocks.

Mistake #3: creating too many threads.
Excessive threads make debugging harder and can reduce performance. One extra thread is one extra hour spent hunting down unexpected bugs.

2
Task
C# SELF, level 55, lesson 2
Locked
Main thread states
Main thread states
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION