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 HandlinglambdaC++MultithreadingthreadingThread 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.