Master C# Programming From Scratch

Clear, interactive, and structured coding lessons designed for absolute beginners.

Multithreading

Multithreading is a programming technique in which an application uses multiple threads to perform different pieces of work. A thread is an independent path of execution inside a process.

Key Idea: A multithreaded application can have multiple threads working independently or cooperatively within the same process.

What is a Thread?

A thread is the smallest sequence of programmed instructions that can be managed independently by the operating system and runtime.

When a C# application starts, it has at least one thread called the main thread. Additional threads can be created when an application needs concurrent execution.

Simple Example: Think of a software application as an office. Instead of one employee doing every task, multiple employees can work on different tasks. Each employee represents a thread.

Process vs Thread

Process Thread
An executing application An execution path inside a process
Has its own memory space Shares process resources
Generally heavier Generally lighter than a process
Can contain multiple threads Belongs to a process

Single Thread Execution

In a simple program, statements normally execute one after another on the main thread.

C#
using System;
 
class Program
{
    static void Main()
    {
        Console.WriteLine("Task 1");
        Console.WriteLine("Task 2");
        Console.WriteLine("Task 3");
    }
}

Output

Output
Task 1
Task 2
Task 3

The statements execute sequentially on the same execution path.

Creating a Thread in C#

C# provides the Thread class in the System.Threading namespace for creating and controlling threads.

C#
using System;
 
using System.Threading;

class Program
{
    static void PrintMessage()
    {
        Console.WriteLine("Running on a separate thread");
    }

    static void Main()
    {
        Thread thread = new Thread(PrintMessage);

        thread.Start();

        Console.WriteLine("Main thread is running");
    }
}

The Thread object is created with a method that contains the work. Calling Start() begins execution of that thread.

Note: The order of the two output lines is not guaranteed because the operating system decides when each thread gets execution time.

Thread.Start()

The Start() method begins execution of the thread.

C#
Thread thread = new Thread(PrintMessage);

thread.Start();

Creating a Thread object does not start its execution. Start() is required to begin the thread.

Thread.Sleep()

Thread.Sleep() pauses the current thread for the specified amount of time.

C#
using System;
 
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("Start");

        Thread.Sleep(2000);

        Console.WriteLine("End");
    }
}

Output

Output
Start
End

The current thread pauses for approximately two seconds before executing the next statement.

Thread.Join()

The Join() method makes the calling thread wait until another thread has completed.

C#
using System;
 
using System.Threading;

class Program
{
    static void Worker()
    {
        for (int i = 1; i <= 3; i++)
        {
            Console.WriteLine("Worker: " + i);
            Thread.Sleep(500);
        }
    }

    static void Main()
    {
        Thread thread = new Thread(Worker);

        thread.Start();

        thread.Join();

        Console.WriteLine("Worker thread completed");
    }
}

Output

Output
Worker: 1
Worker: 2
Worker: 3
Worker thread completed

Thread Lifecycle and States

During its lifetime, a thread moves through different execution states. The following diagram shows the basic flow of a thread from creation to completion.

New / Unstarted
Thread object is created
Thread.Start() ↓
Runnable / Ready
Waiting for CPU scheduling
CPU Scheduler ↓
Running
Thread is executing
Thread.Sleep()
Monitor.Wait()
TargetThread.Join()
↓
WaitSleepJoin / Blocked
Waiting, sleeping or blocked
Sleep time ends
Object.Pulse()
Other thread finishes
↑
↑ Returns to Ready
Task finishes / Unhandled exception ↓
Stopped / Dead
Thread execution has ended

The lifecycle can be understood as a sequence of states. A thread is created first, started using Start(), scheduled for execution, and then may temporarily wait before becoming ready to run again.

Important Thread States

  • Unstarted: Thread object is created but Start() has not been called.
  • Runnable / Ready: Thread is ready to execute and is waiting for CPU scheduling.
  • Running: Thread is currently executing its instructions.
  • WaitSleepJoin: Thread is waiting, sleeping, or joining another thread.
  • Stopped: Thread has completed execution.
Remember: A thread can move between runnable, running and waiting states during its lifetime. The exact scheduling order is controlled by the operating system and .NET runtime.

Checking Thread State in C#

C#
using System;
 
using System.Threading;

class Program
{
    static void Work()
    {
        Thread.Sleep(1000);
    }

    static void Main()
    {
        Thread thread = new Thread(Work);

        Console.WriteLine(thread.ThreadState);

        thread.Start();

        Console.WriteLine(thread.ThreadState);

        thread.Join();

        Console.WriteLine(thread.ThreadState);
    }
}

Thread states can change quickly during execution, so the exact state observed at a particular moment depends on timing.

Thread ID

Each thread has a managed thread identifier that can be useful while debugging or tracing concurrent execution.

C#
using System;
 
using System.Threading;

class Program
{
    static void ShowThread()
    {
        Console.WriteLine(
            "Worker Thread ID: " +
            Thread.CurrentThread.ManagedThreadId);
    }

    static void Main()
    {
        Console.WriteLine(
            "Main Thread ID: " +
            Thread.CurrentThread.ManagedThreadId);

        Thread thread = new Thread(ShowThread);

        thread.Start();
        thread.Join();
    }
}

The actual numeric IDs can be different each time the program runs.

Example: Multiple Threads

An application can create multiple threads, with each thread performing a separate piece of work.

C#
using System;
using System.Threading;

class Program
{
    static void TaskOne()
    {
        for (int i = 1; i <= 5; i++)
        {
            Console.WriteLine("Task One: " + i);
            Thread.Sleep(300);
        }
    }

    static void TaskTwo()
    {
        for (int i = 1; i <= 5; i++)
        {
            Console.WriteLine("Task Two: " + i);
            Thread.Sleep(300);
        }
    }

    static void Main()
    {
        Thread thread1 = new Thread(TaskOne);
        Thread thread2 = new Thread(TaskTwo);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Both tasks completed");
    }
}

Possible Output

Output
Task One: 1
Task Two: 1
Task One: 2
Task Two: 2
Task Two: 3
Task One: 3
Task One: 4
Task Two: 4
Task One: 5
Task Two: 5
Both tasks completed
Important: The exact order of output may change between executions because thread scheduling is controlled by the runtime and operating system.

Shared Data and Race Conditions

Multiple threads can access the same data. If two or more threads modify shared data at the same time, the final result may become unpredictable.

This situation is commonly known as a race condition.

C#
using System;
 
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 10000; i++)
        {
            counter++;
        }
    }

    static void Main()
    {
        Thread thread1 = new Thread(Increment);
        Thread thread2 = new Thread(Increment);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Counter = " + counter);
    }
}

It may be tempting to expect exactly 20000, but unsynchronized concurrent updates can produce an unexpected result.

Synchronization Using lock

C# provides the lock statement to protect a critical section so that only one thread at a time can execute that section for the same lock object.

C#
using System;
using System.Threading;

class Program
{
    static int counter = 0;
    static object lockObject = new object();

    static void Increment()
    {
        for (int i = 0; i < 10000; i++)
        {
            lock (lockObject)
            {
                counter++;
            }
        }
    }

    static void Main()
    {
        Thread thread1 = new Thread(Increment);
        Thread thread2 = new Thread(Increment);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Counter = " + counter);
    }
}

Output

Output
Counter = 20000

The lock ensures that the increment operation inside the critical section is protected from simultaneous access by another thread using the same lock object.

Monitor

The Monitor class provides synchronization mechanisms for controlling access to shared resources.

C#
using System;
 
using System.Threading;

class Program
{
    static object lockObject = new object();

    static void PrintMessage()
    {
        Monitor.Enter(lockObject);

        try
        {
            Console.WriteLine("Thread entered critical section");
        }
        finally
        {
            Monitor.Exit(lockObject);
        }
    }

    static void Main()
    {
        Thread thread = new Thread(PrintMessage);

        thread.Start();
        thread.Join();
    }
}

The lock statement is generally simpler to use, while Monitor provides more control over synchronization.

Thread Priority

A thread can have a priority that influences scheduling. Available priorities include:

  • Lowest
  • BelowNormal
  • Normal
  • AboveNormal
  • Highest
C#
Thread thread = new Thread(Work);

thread.Priority = ThreadPriority.AboveNormal;

thread.Start();
Note: Thread priority is only a scheduling hint. It should not be used as a guarantee that one thread will always execute before another.

Background Thread

A thread can be marked as a background thread using the IsBackground property.

C#
Thread thread = new Thread(Work);

thread.IsBackground = true;

thread.Start();

Background threads do not keep the application alive after all foreground threads have ended.

Thread Safety

Code is considered thread-safe when it behaves correctly even when accessed concurrently by multiple threads.

Thread safety becomes important when multiple threads share mutable data or access shared resources.

Common techniques used to improve thread safety include: lock, Monitor, Interlocked, concurrent collections, immutable data, and proper synchronization.

Interlocked

The Interlocked class provides atomic operations for simple shared values such as counters.

C#
using System;
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 10000; i++)
        {
            Interlocked.Increment(ref counter);
        }
    }

    static void Main()
    {
        Thread thread1 = new Thread(Increment);
        Thread thread2 = new Thread(Increment);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Counter = " + counter);
    }
}

Output

Output
Counter = 20000

ThreadPool

Creating a new Thread manually for every small piece of work can be expensive. .NET provides the ThreadPool to manage reusable worker threads.

C#
using System;
 
using System.Threading;

class Program
{
    static void Work(object? state)
    {
        Console.WriteLine("Work executed by ThreadPool");
    }

    static void Main()
    {
        ThreadPool.QueueUserWorkItem(Work);

        Thread.Sleep(1000);
    }
}

The ThreadPool maintains a collection of worker threads that can execute queued work items.

Thread vs ThreadPool

Thread ThreadPool
Created and managed explicitly Managed by .NET
More control Convenient for short work items
Creation has overhead Reuses existing worker threads
Useful when specific thread control is required Useful for general background work

Real-World Uses of Multithreading

Multithreading can be useful in applications that need concurrent execution of independent work.

  • Desktop applications
  • Background processing
  • Logging systems
  • File processing
  • Data processing workloads
  • Server-side applications
  • Applications that perform multiple independent operations
CIIT Practical Point: In real .NET projects, developers should understand threads, synchronization and shared-state problems because concurrent code can introduce difficult bugs if shared resources are not handled correctly.

Common Multithreading Mistakes

1. Ignoring Shared Data

Multiple threads modifying the same variable can create unpredictable results.

2. Incorrect Locking

Poor synchronization can lead to deadlocks, unnecessary blocking, or performance problems.

3. Creating Too Many Threads

Creating large numbers of threads can consume resources and reduce application performance.

4. Assuming Output Order

When multiple threads run concurrently, their output order should not normally be assumed to be fixed.

Multithreading vs Asynchronous Programming

Multithreading Asynchronous Programming
Focuses on concurrent execution using threads Focuses on handling asynchronous operations
Threads are an important part of the model Does not automatically require creating threads
Shared state requires synchronization Often used for I/O-bound operations
Useful for concurrent work Useful for operations that spend time waiting

Interview Questions

1. What is multithreading?

Multithreading is a technique where multiple threads execute different pieces of work within the same process.

2. What is a thread?

A thread is an independent execution path within a process.

3. How do you create a thread in C#?

A thread can be created using the System.Threading.Thread class.

4. What is Thread.Start()?

Start() begins execution of the thread.

5. What is Thread.Join()?

Join() causes the calling thread to wait until the specified thread has completed.

6. What is a race condition?

A race condition occurs when concurrent operations access shared data and the result depends on the timing of execution.

7. What is lock in C#?

The lock statement provides mutual exclusion for a critical section protected by the same lock object.

8. What is ThreadPool?

ThreadPool is a .NET-managed collection of reusable worker threads used for executing queued work.

Practice Programs

  1. Create a thread that prints numbers from 1 to 10.
  2. Create two threads that print different messages.
  3. Use Thread.Join() to wait for a worker thread.
  4. Create a shared counter and observe the race condition.
  5. Fix the shared counter using the lock statement.
  6. Implement the same counter using Interlocked.Increment().
  7. Display the ManagedThreadId of the main and worker threads.

Summary

  • A thread is an independent execution path inside a process.
  • C# provides the Thread class for explicit thread creation.
  • Start() begins thread execution.
  • Join() waits for another thread to finish.
  • A thread moves through different lifecycle states during its execution.
  • Multiple threads can access shared resources.
  • Shared mutable data can cause race conditions.
  • lock can protect a critical section.
  • Interlocked provides atomic operations for certain shared values.
  • ThreadPool provides reusable worker threads managed by .NET.
  • Multithreading requires careful synchronization and resource management.