Fundamentals 13 min read

C# Multithreading Journey (2) – Creating and Starting Threads

This article explains how to create and start threads in C#, covering ThreadStart delegates, lambda expressions, passing parameters, naming threads, foreground/background distinctions, thread priority, and exception handling, with clear code examples and best‑practice recommendations.

Wukong Talks Architecture
Wukong Talks Architecture
Wukong Talks Architecture
C# Multithreading Journey (2) – Creating and Starting Threads

This tutorial demonstrates the fundamentals of multithreading in C# by showing how to create and start threads using the Thread class and the ThreadStart delegate.

Thread creation and start – A thread is instantiated with a ThreadStart delegate (or a method group) and started by calling Start() . Example:

// ThreadStart delegate definition
[ComVisible(true)]
public delegate void ThreadStart();

class ThreadTest {
    static void Main() {
        Thread t = new Thread(new ThreadStart(Go));
        t.Start();
        Go();
        Console.ReadKey();
    }
    static void Go() {
        Console.WriteLine("hello!");
    }
}

The thread runs Go() concurrently with the main thread, producing two near‑simultaneous "hello!" outputs.

Passing data to a thread – Data can be supplied via lambda expressions, anonymous methods, or the ParameterizedThreadStart delegate. Examples:

// Lambda with a single argument
Thread t = new Thread(() => Print("A"));
 t.Start();
static void Print(string message) { Console.WriteLine(message); }

// Multiple arguments using a lambda block
new Thread(() => {
    Console.WriteLine("a");
    Console.WriteLine("b");
}).Start();

// ParameterizedThreadStart
Thread t = new Thread(Print);
 t.Start("A");
static void Print(object obj) {
    string message = (string)obj; // conversion required
    Console.WriteLine(message);
}

When using lambdas, be careful with captured variables; each iteration should capture a separate copy to avoid nondeterministic output.

// Problematic capture
for (int i = 0; i < 10; i++) {
    new Thread(() => Console.Write(i)).Start();
}
// Correct solution
for (int i = 0; i < 10; i++) {
    int temp = i;
    new Thread(() => Console.Write(temp)).Start();
}

Naming threads – Assign a name via the Name property for easier debugging (e.g., Thread.CurrentThread.Name = "Main Thread"; ).

static void Main(string[] args) {
    Thread.CurrentThread.Name = "Main Thread";
    Thread t = new Thread(Go);
    t.Name = "Worker Thread";
    t.Start();
    Go();
    Console.ReadKey();
}
static void Go() {
    Console.WriteLine("Go! The current thread is {0}", Thread.CurrentThread.Name);
}

Foreground vs. background threads – Threads created by the user are foreground by default and keep the process alive; background threads terminate when all foreground threads finish. The IsBackground property controls this behavior.

Thread t = new Thread(() => Console.ReadKey());
if (args.Length > 0) {
    // background thread
    t.IsBackground = true;
}
 t.Start();

Thread priority – The ThreadPriority enum (Lowest, BelowNormal, Normal, AboveNormal, Highest) influences the scheduler’s allocation of CPU time. Raising priority should be done cautiously to avoid starving other threads.

public enum ThreadPriority {
    Lowest = 0,
    BelowNormal = 1,
    Normal = 2,
    AboveNormal = 3,
    Highest = 4,
}

Process priority can be raised with Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; , but this affects the entire application and may impact UI responsiveness.

using (Process p = Process.GetCurrentProcess()) {
    p.PriorityClass = ProcessPriorityClass.High;
}

Exception handling in threads – Exceptions thrown inside a thread are not caught by a surrounding try/catch in the creator method. Each thread should have its own exception handling logic.

static void Main(string[] args) {
    try {
        new Thread(Go).Start();
    } catch (Exception ex) {
        Console.WriteLine("Exception");
    }
    Console.ReadKey();
}
static void Go() {
    throw null; // will terminate the thread unhandled
}

Correct approach: place the try/catch inside the thread method itself.

static void Main(string[] args) {
    new Thread(Go).Start();
    Console.ReadKey();
}
static void Go() {
    try {
        throw null;
    } catch (Exception ex) {
        Console.WriteLine(ex.Message);
    }
}

For UI applications, global handlers like Application.ThreadException only catch UI thread exceptions; background thread exceptions must be handled manually or via AppDomain.CurrentDomain.UnhandledException , which cannot prevent process termination.

Exception HandlinglambdaCMultithreadingthreadingThread PriorityThreadStart
Wukong Talks Architecture
Written by

Wukong Talks Architecture

Explaining distributed systems and architecture through stories. Author of the "JVM Performance Tuning in Practice" column, open-source author of "Spring Cloud in Practice PassJava", and independently developed a PMP practice quiz mini-program.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.