多线程是提升 C# 程序并发能力的核心手段,适用于处理耗时操作(如 IO、网络请求、计算密集型任务)。本文从实战角度讲解多线程的创建、管理和优雅终止,全程使用可直接运行的代码示例,贴合新手学习习惯。
前置准备
所有多线程操作都依赖 System.Threading 命名空间,首先在代码中引入:
csharp
using System;
using System.Threading;
// 后续用到Task/CancellationToken时需要
using System.Threading.Tasks;
环境要求:.NET Framework 4.0+ / .NET Core 2.0+ / .NET 5+(所有现代 C# 环境均支持)。
一、线程创建:从基础到推荐方式
C# 中创建线程主要有 3 种方式,优先级:Task(推荐)> Thread(基础)> ThreadPool(底层)。
1. 基础方式:Thread 类(无参数)
使用 ThreadStart 委托创建无参数线程,核心方法是 Start() 启动线程。
csharp
// 示例:创建无参数线程
class ThreadCreationDemo
{
static void Main()
{
// 1. 定义线程要执行的方法
void PrintNumbers()
{
for (int i = 1; i <= 5; i++)
{
// Thread.CurrentThread.ManagedThreadId:获取当前线程ID
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId}:数字 {i}");
// 模拟耗时操作(暂停500毫秒)
Thread.Sleep(500);
}
}
// 2. 创建线程实例
Thread thread = new Thread(PrintNumbers);
// 3. 启动线程(此时线程进入就绪状态,等待CPU调度)
thread.Start();
// 主线程继续执行(验证并发)
Console.WriteLine($"主线程 {Thread.CurrentThread.ManagedThreadId}:我在主线程执行");
// 等待子线程执行完毕(否则主线程退出会直接结束程序)
thread.Join();
}
}
运行结果(线程 ID 可能不同,顺序非固定):
主线程 1:我在主线程执行
线程 3:数字 1
线程 3:数字 2
线程 3:数字 3
线程 3:数字 4
线程 3:数字 5
2. Thread 类(带参数)
使用 ParameterizedThreadStart 委托,参数必须是 object 类型(需手动转换)。
csharp
static void Main()
{
// 带参数的线程方法
void PrintMessage(object msgObj)
{
string message = msgObj as string;
if (message == null) message = "默认消息";
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId}:{message}");
Thread.Sleep(1000);
}
// 创建线程并传入参数
Thread thread = new Thread(PrintMessage);
// 启动时传入参数
thread.Start("Hello 多线程!");
thread.Join();
}
3. 推荐方式:Task(基于线程池,更高效)
Task 是 .NET 4.0+ 推出的现代多线程方案,底层复用线程池,避免手动创建线程的开销,支持异步编程(async/await)。
csharp
static void Main()
{
// 方式1:创建并启动Task
Task task1 = Task.Run(() =>
{
Console.WriteLine($"Task线程 {Thread.CurrentThread.ManagedThreadId}:执行任务1");
Thread.Sleep(1000);
});
// 方式2:带返回值的Task
Task<int> task2 = Task.Run(() =>
{
Console.WriteLine($"Task线程 {Thread.CurrentThread.ManagedThreadId}:执行计算任务");
Thread.Sleep(1000);
return 100 + 200;
});
// 等待所有Task完成
Task.WaitAll(task1, task2);
// 获取带返回值Task的结果
Console.WriteLine($"计算结果:{task2.Result}");
}
核心优势 :Task 自动管理线程生命周期,无需手动调用 Join(可通过 Wait/WaitAll 等待),支持取消、异常捕获等高级特性。
二、线程管理:核心技巧
1. 等待线程完成(Join/Wait)
Thread.Join():阻塞当前线程,直到目标线程执行完毕(适用于 Thread 类)。Task.Wait():阻塞当前线程,直到 Task 完成(适用于 Task 类)。
csharp
static void Main()
{
Thread t = new Thread(() =>
{
Thread.Sleep(2000);
Console.WriteLine("子线程执行完毕");
});
t.Start();
Console.WriteLine("等待子线程完成...");
// 主线程阻塞,直到t执行完毕
t.Join();
Console.WriteLine("主线程继续执行");
}
2. 前台/后台线程
- 前台线程:默认类型,只要有一个前台线程运行,进程就不会退出。
- 后台线程:进程退出时会强制终止,适合执行非关键任务(如日志收集)。
csharp
static void Main()
{
Thread t = new Thread(() =>
{
// 无限循环(验证后台线程行为)
while (true)
{
Console.WriteLine("后台线程运行中...");
Thread.Sleep(1000);
}
});
// 设置为后台线程
t.IsBackground = true;
t.Start();
// 主线程暂停3秒后退出
Thread.Sleep(3000);
Console.WriteLine("主线程退出,进程结束");
}
结果:主线程退出后,后台线程被强制终止,不会无限输出。
3. 线程优先级
通过 Thread.Priority 设置优先级(影响 CPU 调度优先级,不保证执行顺序 ),可选值:Lowest < BelowNormal < Normal < AboveNormal < Highest。
csharp
static void Main()
{
Thread t1 = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("低优先级线程:" + i);
Thread.Sleep(100);
}
});
t1.Priority = ThreadPriority.Lowest;
Thread t2 = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("高优先级线程:" + i);
Thread.Sleep(100);
}
});
t2.Priority = ThreadPriority.Highest;
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
4. 线程本地存储(ThreadLocal)
解决多线程共享变量的问题,每个线程拥有独立的变量副本:
csharp
static void Main()
{
// 线程本地存储:每个线程有自己的count副本
ThreadLocal<int> count = new ThreadLocal<int>(() => 0);
// 启动3个线程,各自累加count
for (int i = 0; i < 3; i++)
{
new Thread(() =>
{
for (int j = 0; j < 5; j++)
{
count.Value++;
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId}:count = {count.Value}");
}
// 释放资源
count.Dispose();
}).Start();
}
Thread.Sleep(2000);
}
三、线程终止:优雅终止而非强制杀死
1. 绝对避免:Thread.Abort()(已废弃)
Abort() 会强制抛出 ThreadAbortException 终止线程,可能导致:
- 资源泄漏(如未释放文件句柄、数据库连接);
- 数据损坏(线程执行到一半被终止);
- .NET Core/.NET 5+ 中已标记为废弃,不推荐使用。
2. 推荐方式1:布尔标志位(简单场景)
通过共享布尔变量控制线程循环,实现优雅终止:
csharp
class GracefulStopDemo
{
// 线程终止标志(必须用volatile,保证多线程可见性)
private static volatile bool _isStop = false;
static void Main()
{
Thread workerThread = new Thread(Work);
workerThread.Start();
// 主线程等待5秒后,触发终止
Console.WriteLine("5秒后终止子线程...");
Thread.Sleep(5000);
_isStop = true;
workerThread.Join();
Console.WriteLine("子线程已优雅终止");
}
static void Work()
{
int count = 0;
// 循环判断终止标志
while (!_isStop)
{
Console.WriteLine($"子线程运行中,计数:{count++}");
Thread.Sleep(1000);
}
// 终止前的清理工作(释放资源、保存数据等)
Console.WriteLine("子线程执行清理工作...");
}
}
关键 :volatile 关键字确保多个线程能实时看到变量的最新值,避免缓存导致的判断失效。
3. 推荐方式2:CancellationToken(规范场景)
CancellationToken 是 .NET 官方推荐的取消机制,支持批量取消、注册回调等高级功能,适配 Task/Thread。
csharp
static void Main()
{
// 1. 创建取消令牌源
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// 2. 启动线程/Task,传入取消令牌
Task workerTask = Task.Run(() =>
{
int count = 0;
while (!token.IsCancellationRequested)
{
Console.WriteLine($"Task运行中,计数:{count++}");
Thread.Sleep(1000);
}
// 可选:抛出取消异常(符合.NET取消规范)
token.ThrowIfCancellationRequested();
}, token);
// 3. 3秒后触发取消
Console.WriteLine("3秒后取消Task...");
Thread.Sleep(3000);
cts.Cancel(); // 发送取消信号
try
{
workerTask.Wait();
}
catch (AggregateException ex)
{
// 捕获取消异常(正常流程,无需处理)
if (ex.InnerException is TaskCanceledException)
{
Console.WriteLine("Task已被优雅取消");
}
else
{
// 处理其他异常
Console.WriteLine("异常:" + ex.Message);
}
}
finally
{
// 释放令牌源资源
cts.Dispose();
}
}
优势 :支持注册取消回调(token.Register(() => { 清理逻辑 })),适合复杂场景(如多任务批量取消)。
总结
关键点回顾
- 创建线程 :优先使用
Task(高效、易管理),仅在需精细控制线程(如前台/后台、优先级)时使用Thread类。 - 管理线程 :通过
Join/Wait等待线程完成,后台线程不会阻塞进程退出,ThreadLocal<T>解决线程共享变量问题。 - 终止线程 :绝对避免
Abort(),简单场景用volatile布尔标志位,规范场景用CancellationToken实现优雅终止,终止前务必执行资源清理。
最佳实践
- 耗时 IO 操作(如网络请求、文件读写)优先使用
async/await(基于 Task 的异步编程),而非手动创建线程; - 计算密集型任务可使用
Task+ 线程池,避免创建大量 Thread 导致的资源耗尽; - 所有多线程操作需注意线程安全(如锁
lock、原子操作Interlocked),避免竞态条件。