目 录CONTENT

文章目录

CSharp(五十六) 线程(Thread)详解

C# 线程(Thread)详解


一、什么是线程?

1.1 生活比喻

进程 = 一家餐厅(有独立的厨房、餐桌、收银台)
线程 = 餐厅里的一个员工(服务员、厨师、收银员)

一个餐厅(进程)里可以有多个员工(线程)同时工作——厨师在炒菜、服务员在端盘子、收银员在结账。
他们都是在这个餐厅里工作,共用同一个厨房、同一些餐具(共享进程的内存)。

主线程相当于老板——程序启动就有,负责主要工作。
子线程相当于雇来的员工——老板分派任务,让员工去干。

1.2 为什么需要多线程?

// 没有多线程——程序卡住了!
static void Main()
{
    DownloadFile("https://example.com/bigfile.zip");  // 下载要 10 秒
    ShowProgressBar();                                  // 等下载完才能显示进度条
    // 用户感觉程序"死了"——界面卡住不动
}

// 有多线程——同时进行!
static void Main()
{
    Thread downloadThread = new Thread(() => DownloadFile(...));
    downloadThread.Start();     // 让子线程去下载

    // 主线程继续跑——显示进度条,响应用户操作
    ShowProgressBar();
}

1.3 核心概念速览

概念 说明
进程 运行中的程序,有自己的内存空间
线程 进程中的执行单元,共享进程内存
主线程 程序启动时自动创建的线程
子线程 代码中手动创建的线程
多线程 多个线程并发执行
线程安全 多线程同时访问数据不会出错

二、创建和启动线程

2.1 最基础的方式 —— Thread 类

using System;
using System.Threading;

class Program
{
    // 这个方法将在线程中执行
    static void PrintNumbers()
    {
        for (int i = 1; i <= 5; i++)
        {
            Console.WriteLine($"子线程: {i}");
            Thread.Sleep(500);  // 暂停 500 毫秒
        }
    }

    static void Main()
    {
        Console.WriteLine("主线程开始");

        // 创建线程——告诉它要执行哪个方法
        Thread thread = new Thread(PrintNumbers);

        // 启动线程
        thread.Start();

        // 主线程继续执行
        for (int i = 1; i <= 3; i++)
        {
            Console.WriteLine($"主线程: {i}");
            Thread.Sleep(300);
        }

        Console.WriteLine("主线程结束");
    }
}

输出(每次运行顺序可能不同):

主线程开始
主线程: 1
子线程: 1
主线程: 2
子线程: 2
主线程: 3
主线程结束
子线程: 3
子线程: 4
子线程: 5

注意:主线程做完了子线程还可能继续跑!它们各跑各的。

2.2 传参数给线程 —— ParameterizedThreadStart

// 方式一:ParameterizedThreadStart(参数是 object 类型)
static void PrintMessage(object msg)
{
    string message = (string)msg;  // 需要强转
    Console.WriteLine($"收到消息: {message}");
}

Thread t1 = new Thread(PrintMessage);
t1.Start("Hello from Main!");


// 方式二:Lambda 表达式(推荐!更灵活)
Thread t2 = new Thread(() =>
{
    string name = "张三";
    int age = 18;
    Console.WriteLine($"{name} 今年 {age} 岁");
});
t2.Start();


// 方式三:Lambda 捕获外部变量
string city = "北京";
int score = 95;
Thread t3 = new Thread(() =>
{
    Console.WriteLine($"{city} 的学生考了 {score} 分");
});
t3.Start();

2.3 五种创建方式汇总

// 1. ThreadStart 委托(无参方法)
static void DoWork() { }
Thread t1 = new Thread(DoWork);

// 2. ThreadStart + Lambda
Thread t2 = new Thread(() => { });

// 3. ParameterizedThreadStart(object 参数)
static void DoWork2(object obj) { }
Thread t3 = new Thread(DoWork2);

// 4. ParameterizedThreadStart + Lambda
Thread t4 = new Thread(obj => { });

// 5. 直接 new Thread 传 Lambda(最常用)
Thread t5 = new Thread(() =>
{
    Console.WriteLine("子线程工作!");
});

三、线程的关键操作

3.1 等待线程结束 —— Join

static void Main()
{
    Console.WriteLine("主线程: 开始");

    Thread worker = new Thread(() =>
    {
        Console.WriteLine("子线程: 工作开始...");
        Thread.Sleep(2000);  // 模拟耗时工作
        Console.WriteLine("子线程: 工作完成!");
    });

    worker.Start();

    // 等子线程干完再继续
    worker.Join();  // 主线程停在这里,等 worker 结束

    Console.WriteLine("主线程: 子线程已完成,我继续了");
}

输出:

主线程: 开始
子线程: 工作开始...
子线程: 工作完成!      ← 等了 2 秒
主线程: 子线程已完成,我继续了

Join 带超时:

// 最多等 3 秒,超时就继续
if (worker.Join(3000))
{
    Console.WriteLine("子线程在 3 秒内完成了");
}
else
{
    Console.WriteLine("超时!不等了,继续执行");
}

3.2 暂停线程 —— Sleep

Thread.Sleep(1000);      // 当前线程暂停 1000 毫秒(1 秒)
Thread.Sleep(0);         // 让出 CPU 时间片给其他线程
Thread.Sleep(Timeout.Infinite);  // 永久休眠(几乎不用)

注意Sleep 只能让当前线程休眠,不能用来让别的线程休眠。

3.3 线程的前后台属性 —— IsBackground

Thread t = new Thread(() =>
{
    while (true)
    {
        Console.WriteLine("工作中...");
        Thread.Sleep(1000);
    }
});

// t.IsBackground = false;  // 默认——前台线程
// 程序会一直运行,死循环不会退出

t.IsBackground = true;       // 后台线程
// 主线程结束后,后台线程自动终止,程序退出
属性 前台线程(默认) 后台线程(IsBackground = true)
主线程结束时 子线程继续跑 子线程被强制终止
适用场景 重要工作,必须完成 辅助工作,跟着主线程走

3.4 获取当前线程信息

Thread current = Thread.CurrentThread;

Console.WriteLine($"线程 ID: {current.ManagedThreadId}");
Console.WriteLine($"线程名称: {current.Name}");
Console.WriteLine($"是否后台: {current.IsBackground}");
Console.WriteLine($"是否存活: {current.IsAlive}");
Console.WriteLine($"线程池线程: {current.IsThreadPoolThread}");

3.5 给线程起名字

Thread t1 = new Thread(DoWork);
t1.Name = "下载线程";     // 给线程起个名,调试时好辨认

Thread t2 = new Thread(DoWork);
t2.Name = "上传线程";

四、线程安全问题——多线程的"雷区"

4.1 什么是线程安全问题?

多个线程同时修改同一个数据,导致数据错乱。

static int counter = 0;

static void Main()
{
    for (int i = 0; i < 10; i++)
    {
        new Thread(() =>
        {
            for (int j = 0; j < 1000; j++)
            {
                counter++;  // ⚠️ 不是原子操作!线程不安全!
            }
        }).Start();
    }

    Thread.Sleep(2000);  // 等所有线程跑完
    Console.WriteLine($"预期: 10000, 实际: {counter}");
    // 实际可能输出: 9823, 9876, 10000... 每次不一样!
}

为什么 counter++ 不安全? 它其实分三步:

1. 读取 counter 的值到 CPU 寄存器
2. 把值 +1
3. 把新值写回 counter

线程 A                   线程 B
读取 counter = 10
                         读取 counter = 10(也是 10!)
+1 → 11
                         +1 → 11
写回 counter = 11
                         写回 counter = 11(覆盖了!)
结果:两次 ++,counter 只加了 1!

4.2 解决方案——lock 锁

static int counter = 0;
static object lockObj = new object();  // 锁对象

static void Main()
{
    for (int i = 0; i < 10; i++)
    {
        new Thread(() =>
        {
            for (int j = 0; j < 1000; j++)
            {
                lock (lockObj)    // 加锁——一次只有一个线程能进来
                {
                    counter++;    // ✅ 现在安全了!
                }
            }
        }).Start();
    }

    Thread.Sleep(2000);
    Console.WriteLine($"预期: 10000, 实际: {counter}");
    // 实际输出: 10000 ✅ 正确!
}

lock 的工作原理:

lock (lockObj) 相当于:
┌─────────────────────────────┐
│  检查 lockObj 有没有人锁着   │
│  ├─ 没人 → 锁上,进去执行    │
│  └─ 有人 → 排队等            │
│  执行完 → 开锁,下一个进去   │
└─────────────────────────────┘

4.3 lock 的使用规则

// ✅ 锁对象必须是引用类型
static object locker = new object();

// ❌ 不能锁值类型
// lock (123) { }  // 编译错误!

// ❌ 不能锁 null
// object obj = null; lock (obj) { }  // 运行时错误!

// ❌ 不要锁 this 或 typeof 或字符串字面量(可能被其他代码锁)
// lock (this) { }      // 不推荐
// lock (typeof(Program)) { }  // 不推荐
// lock ("myLock") { }  // 不推荐(字符串会被驻留)

// ✅ 用 private static readonly object
private static readonly object _lock = new object();

五、线程安全的其他方式

5.1 Interlocked —— 原子操作(轻量级)

static int counter = 0;

// Interlocked 提供硬件级别的原子操作,比 lock 快
Interlocked.Increment(ref counter);     // 原子地 +1
Interlocked.Decrement(ref counter);     // 原子地 -1
Interlocked.Add(ref counter, 5);        // 原子地 +5
Interlocked.Exchange(ref counter, 100); // 原子地设值,返回旧值

int oldValue = Interlocked.CompareExchange(ref counter, 100, 50);
// 如果 counter == 50,就改成 100,无论是否改了都返回旧值

5.2 Monitor —— lock 的底层实现

// lock 语句编译后就是 Monitor
lock (lockObj)
{
    // 临界区
}

// 等价于
Monitor.Enter(lockObj);
try
{
    // 临界区
}
finally
{
    Monitor.Exit(lockObj);
}

// Monitor 比 lock 多了 TryEnter(尝试获取锁,不阻塞)
if (Monitor.TryEnter(lockObj, 1000))  // 1 秒超时
{
    try
    {
        // 拿到锁了
    }
    finally
    {
        Monitor.Exit(lockObj);
    }
}
else
{
    Console.WriteLine("没拿到锁,不等了");
}

5.3 ManualResetEvent —— 线程间发信号

// 场景:子线程干完活通知主线程
ManualResetEvent signal = new ManualResetEvent(false);

Thread worker = new Thread(() =>
{
    Console.WriteLine("子线程:工作中...");
    Thread.Sleep(2000);
    Console.WriteLine("子线程:干完了!");
    signal.Set();  // 发信号——我搞定了!
});
worker.Start();

Console.WriteLine("主线程:等子线程的信号...");
signal.WaitOne();  // 阻塞,等信号
Console.WriteLine("主线程:收到信号,继续执行");

输出:

主线程:等子线程的信号...
子线程:工作中...
子线程:干完了!
主线程:收到信号,继续执行

六、线程池(ThreadPool)—— 不用自己 new 线程

6.1 什么是线程池?

线程池就是预先创建好的一群线程,你要用直接取,用完还回去——省去了反复创建/销毁线程的开销。

手动创建线程:      雇一个临时工,干完活开除,下次再雇(开销大)
线程池:            有一个固定团队,来活了分配,干完回收(开销小)

6.2 基本用法

// 用线程池执行任务——不需要 new Thread
ThreadPool.QueueUserWorkItem(state =>
{
    Console.WriteLine($"线程池线程 ID: {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"参数: {state}");
}, "Hello ThreadPool");

6.3 手动 vs 线程池

特性 手动 new Thread 线程池
创建开销 大(每次都创建新线程) 小(复用线程)
线程数量 可控 系统自动管理
线程名称 可以设置 不能设置
前台/后台 默认前台 总是后台
适用场景 长期运行的任务 短小频繁的任务
返回值 不方便 不方便(用 Task 更方便)

七、Task —— 现代 C# 推荐的方式

7.1 Task 比 Thread 好在哪?

// Thread 方式
Thread t = new Thread(() =>
{
    int result = Calculate();
    Console.WriteLine(result);
});
t.Start();
// 问题:返回值不好拿,异常处理麻烦

// Task 方式
Task<int> task = Task.Run(() => Calculate());
int result = task.Result;  // 直接拿到返回值!
// Task 还支持 await(异步)、异常捕获更友好

建议:新代码用 Task 代替 Thread。Thread 是底层 API,Task 是现代高层的 API,更好用。

7.2 Task 基本用法

using System.Threading.Tasks;

// 创建并启动任务
Task task1 = Task.Run(() =>
{
    Console.WriteLine("Task 工作中...");
});

// 带返回值
Task<int> task2 = Task.Run(() =>
{
    Thread.Sleep(1000);
    return 42;
});

int result = task2.Result;  // 获取结果(会等待任务完成)
Console.WriteLine($"结果: {result}");

// 等待完成
task2.Wait();  // 等效于 Thread.Join

// 批量启动
Task[] tasks = new Task[3];
for (int i = 0; i < 3; i++)
{
    int taskId = i;
    tasks[i] = Task.Run(() =>
    {
        Console.WriteLine($"任务 {taskId} 完成");
    });
}

Task.WaitAll(tasks);  // 等全部完成
Console.WriteLine("所有任务完成");

八、实战示例——多线程下载模拟

using System;
using System.Collections.Generic;
using System.Threading;

class Program
{
    // 线程安全的结果收集器
    private static List<string> _results = new List<string>();
    private static readonly object _lock = new object();
    private static int _completedCount = 0;

    static void DownloadFile(string url)
    {
        Console.WriteLine($"[线程 {Thread.CurrentThread.ManagedThreadId}] 开始下载: {url}");

        // 模拟下载耗时(随机 1~3 秒)
        Random rand = new Random();
        int downloadTime = rand.Next(1000, 3000);
        Thread.Sleep(downloadTime);

        // 线程安全地保存结果
        lock (_lock)
        {
            _results.Add($"{url} - 下载完成 ({downloadTime / 1000.0:F1}秒)");
            _completedCount++;
        }

        Console.WriteLine($"[线程 {Thread.CurrentThread.ManagedThreadId}] 完成: {url}");
    }

    static void Main()
    {
        Console.WriteLine("===== 多线程下载管理器 =====\n");

        string[] urls =
        {
            "文件1.zip", "文件2.zip", "文件3.zip",
            "文件4.zip", "文件5.zip"
        };

        // 创建多个下载线程
        List<Thread> threads = new List<Thread>();
        foreach (string url in urls)
        {
            Thread t = new Thread(() => DownloadFile(url));
            t.Name = $"下载-{url}";
            t.Start();
            threads.Add(t);
        }

        // 实时显示进度(主线程)
        while (_completedCount < urls.Length)
        {
            lock (_lock)
            {
                Console.WriteLine($"\r进度: {_completedCount}/{urls.Length}");
            }
            Thread.Sleep(500);
        }

        // 等待全部完成
        foreach (Thread t in threads)
        {
            t.Join();
        }

        // 显示结果
        Console.WriteLine("\n===== 下载完成 =====\n");
        foreach (string result in _results)
        {
            Console.WriteLine($"  ✅ {result}");
        }
    }
}

输出示例:

===== 多线程下载管理器 =====

[线程 3] 开始下载: 文件1.zip
[线程 4] 开始下载: 文件2.zip
[线程 5] 开始下载: 文件3.zip
[线程 6] 开始下载: 文件4.zip
[线程 7] 开始下载: 文件5.zip
进度: 2/5
[线程 5] 完成: 文件3.zip
进度: 3/5
[线程 4] 完成: 文件2.zip
[线程 6] 完成: 文件4.zip
[线程 7] 完成: 文件5.zip
[线程 3] 完成: 文件1.zip

===== 下载完成 =====

  ✅ 文件3.zip - 下载完成 (1.2秒)
  ✅ 文件2.zip - 下载完成 (1.5秒)
  ✅ 文件4.zip - 下载完成 (2.1秒)
  ✅ 文件5.zip - 下载完成 (2.3秒)
  ✅ 文件1.zip - 下载完成 (2.8秒)

九、常见易错点(避坑指南)

坑1:忘记线程正在跑,程序提前退出

// ❌ 子线程是前台线程,循环内跑不完程序不会退 → 那倒没问题
//    但 Join 能保证顺序

// ✅ 用 Join 等待
Thread t = new Thread(() => { Thread.Sleep(3000); });
t.IsBackground = true;  // 或用后台线程
t.Start();               // 不 Join 也不 wait,程序直接退出,子线程被 kill

坑2:lock 锁的对象不能用 public

// ❌ public 的对象可能被外部代码锁
public object LockObj = new object();

// ✅ 用 private 的
private static readonly object _lock = new object();

坑3:在多线程中更新 UI 控件

// ❌ 直接在子线程更新 UI(WinForms/WPF)
// textBox1.Text = "从子线程更新";  // 抛异常!

// ✅ 用 Invoke 回到 UI 线程
textBox1.Invoke(new Action(() =>
{
    textBox1.Text = "从子线程更新";
}));

坑4:Lambda 捕获循环变量

// ❌ 经典闭包陷阱
for (int i = 0; i < 5; i++)
{
    new Thread(() => Console.WriteLine(i)).Start();
}
// 输出可能是: 5 5 5 5 5 ← 全是 5!因为 i 是同一个变量

// ✅ 用局部变量捕获
for (int i = 0; i < 5; i++)
{
    int copy = i;  // 每次循环创建新变量
    new Thread(() => Console.WriteLine(copy)).Start();
}
// 输出: 1 4 3 0 2(顺序不定,但数字对)

坑5:Abort 已经过时

// ❌ Thread.Abort() 在 .NET Core/5+ 中会抛 PlatformNotSupportedException
// thread.Abort();  // 已过时!

// ✅ 用 CancellationToken 优雅取消
CancellationTokenSource cts = new CancellationTokenSource();
Task.Run(() =>
{
    while (!cts.Token.IsCancellationRequested)
    {
        // 工作中...
    }
}, cts.Token);
cts.Cancel();  // 请求取消

坑6:死锁——两个线程互相等对方释放锁

static object lockA = new object();
static object lockB = new object();

// ❌ 死锁示例
Thread t1 = new Thread(() =>
{
    lock (lockA)
    {
        Thread.Sleep(100);
        lock (lockB) { }  // t1 等 lockB
    }
});

Thread t2 = new Thread(() =>
{
    lock (lockB)
    {
        Thread.Sleep(100);
        lock (lockA) { }  // t2 等 lockA
    }
});
// t1 握着 lockA 等 lockB,t2 握着 lockB 等 lockA → 死锁!

// ✅ 避免死锁:始终按相同顺序获取锁

十、总结

线程操作速查

创建线程:    Thread t = new Thread(() => { });
启动:        t.Start();
等待完成:    t.Join(); 或 t.Join(超时毫秒);
休眠:        Thread.Sleep(毫秒);
前台/后台:   t.IsBackground = true;
线程名:      t.Name = "名字";
当前线程:    Thread.CurrentThread

线程安全速查

场景 方案 性能
简单计数 Interlocked.Increment
保护代码块 lock (obj) { }
需要超时 Monitor.TryEnter
线程间通知 ManualResetEvent
现代异步 Task + async/await

记忆口诀

线程就像多窗口,同时运行不停留
Start 启动各跑各,Join 等待一起走
Sleep 暂停一小会儿,IsBackground 后台溜

多线程里要小心,共享数据会打架
lock 锁住关键区,一次一人稳当当
新代码用 Task 好,async await 最时尚

一句话总结:线程让程序能同时做多件事——用 new Thread 创建、Start 启动、Join 等待。多线程最大的坑是共享数据并发修改,用 lockInterlocked 保护关键区域。现代 C# 推荐用 Task 代替 Thread,用 async/await 写异步代码。

0

评论区