C# 同步异步与多线程:Thread / Task / 线程池一次讲透(上篇)

C# 同步异步与多线程:Thread / Task / 线程池一次讲透(上篇)

程序卡顿、界面无响应,几乎都是"同步阻塞"惹的祸。这课从同步 vs 异步 讲起,把 Thread 多线程、Task 异步、线程池、线程状态、跨线程更新 UI 一次讲透,再配一个递归遍历文件的实战。(下篇讲 IComparer 自定义排序 + FileList 文件管理器案例)


一、同步与异步:先搞清"调度和等待"

同步和异步是编程中两种不同的代码执行模式 ,核心区别就是调度和等待的问题:

  • 同步 :顺序执行,上一个做完才做下一个。如果某个操作很耗时,程序就会卡顿、响应变慢;
  • 异步 :把耗时任务交给别的线程,当前线程不等待,继续往下走。

同步阻塞的例子:

csharp 复制代码
private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("按钮1被点击了,开始工作");
    // 耗时的任务,占用内存(1 亿次循环)
    for (int i = 0; i < 1_0000_0000; i++) { Console.WriteLine(6666666); }
    // 因为和上面的 for 在同一个同步代码块中,必须等它执行完
    MessageBox.Show("一天的工作结束了");
}

特点:顺序执行;但遇到耗时操作就会卡顿。这是同步最大的痛点。


二、用 Thread 解决耗时任务

线程就像多请了几个"干活的人",专门处理耗时任务:

csharp 复制代码
private void button2_Click(object sender, EventArgs e)
{
    MessageBox.Show("邵帛点餐");
    // 线程触发器:委托比较耗时的任务
    ThreadStart start = new ThreadStart(Test);
    // 用线程处理耗时任务,相当于另开辟资源
    Thread thread = new Thread(start);
    thread.Start();            // 启动线程
    MessageBox.Show("张凯点餐");   // 这行不会等 Test 执行完
}

private void Test()
{
    for (int i = 0; i < 1_0000_0000; i++) { Console.WriteLine("袁士博点餐"); }
}

线程的三种开启写法(从繁到简):

csharp 复制代码
// 写法1:ThreadStart 委托
ThreadStart start = new ThreadStart(TestMethod);
Thread t1 = new Thread(start);
t1.Start();

// 写法2:直接传方法
t1 = new Thread(TestMethod);
t1.Start();

// 写法3:匿名委托(最简单)
Thread t2 = new Thread(() => Console.WriteLine("匿名委托开启分线程"));
t2.Start();

线程里算斐波那契(递归)

csharp 复制代码
Thread t3 = new Thread(() =>
{
    int res = Fac(5);       // 在分线程里算
    MessageBox.Show(res.ToString());
});
t3.Start();

// 斐波那契数列:递归的一种表现
// 遇到功能一样、只有数据变化时,用自身调用自身来完成业务逻辑
private int Fac(int n)
{
    if (n <= 1) return n;
    return Fac(n - 1) + Fac(n - 2);
}

三、跨线程更新 UI:必须用 Invoke

铁律:UI 控件只能由创建它的线程(UI 线程)更新。分线程想改界面,必须回到主线程:

csharp 复制代码
private void button4_Click(object sender, EventArgs e)
{
    Thread t4 = new Thread(() =>
    {
        int result = Fac(10);
        // 不加 Invoke 会报错:线程间操作无效,从不是创建控件"label1"的线程访问它
        Invoke(new Action(() => label1.Text = result.ToString()));
    });
    t4.Start();
}

四、线程的停止与状态

线程的生命周期管理(4 个常用方法):

csharp 复制代码
t1.Abort();     // 抛异常终止线程(try-catch 包住)
t1.Join();      // 阻塞当前线程,等待它停止
t1.Suspend();   // 挂起线程,达到"停止"效果
t1.Resume();    // 跟 Suspend 配套,恢复继续

Abort 终止 + 出错重置:

csharp 复制代码
private void button3_Click(object sender, EventArgs e)
{
    try
    {
        if (t1 != null)
        {
            t1.Abort();     // 抛异常终止
            t1 = null;
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show($"线程终止出现了错误:{ex.Message}");
        Thread.ResetAbort();   // 终止出错,取消掉所有终止
    }
}

线程状态(ThreadState):

csharp 复制代码
Thread t11 = new Thread(PrintNumWidthDelay);
Console.WriteLine($"t11 的状态为:{t11.ThreadState}");   // Unstarted
t11.Start();
// ... 运行中会是 Running
Thread.Sleep(TimeSpan.FromSeconds(6));
t11.Abort();
Console.WriteLine($"休眠后 t11 的状态为:{t11.ThreadState}");
Console.WriteLine(t11.IsAlive);   // 是否"活着",true 表示还活着

常见状态:Unstarted(未启动)→ Running(运行中)→ Aborted(已终止);IsAlive 判断是否存活。


五、线程池:把线程放进"池子"统一管理

线程池 把多个线程放到一个"池子"里,好处是按顺序、方便管理、提高效率 。用 ThreadPool.QueueUserWorkItem 添加任务:

csharp 复制代码
// public delegate void WaitCallback(object state);
ThreadPool.QueueUserWorkItem((s) => Console.WriteLine($"{s}"), "你好");
ThreadPool.QueueUserWorkItem((s) =>
{
    for (int i = 0; i < 10; i++)
        Console.WriteLine($"十个线程任务都放到了池子中{i + 1}");
}, 0);
// 比直接 new Thread 要方便

用 ManualResetEvent 控制任务顺序

场景:任务 A 干完,任务 B 才能开始(发信号)。

csharp 复制代码
// 重置当前事件,相当于准备一个"空的事件"
ManualResetEvent resetEvent = new ManualResetEvent(false);

ThreadPool.QueueUserWorkItem(o =>
{
    for (int i = 0; i < 20; i++)
    {
        DoSomething(o.ToString());
        Thread.Sleep(1);
    }
    resetEvent.Set();   // 发信号:A 干完了
}, "上料");

resetEvent.WaitOne();   // 阻塞线程,直到收到信号才疏通

ThreadPool.QueueUserWorkItem(o =>
{
    for (int i = 0; i < 20; i++) DoSomething(o.ToString());
}, "检测");

ManualResetEvent(false):初始无信号 → 任务完成后 Set() 发信号 → WaitOne() 等待信号放行。这就是让"上料"完成后再"检测"的顺序控制。


六、Task:更高级的异步

Task 可以理解为结合了 Action 和 Func ,是对线程池的封装,提供异步处理和任务组合能力,适合密集型任务。

带返回值的 Task + ContinueWith(链式)

csharp 复制代码
// 相当于 Func 的 Task,返回类型是 int
Task<int> t = new Task<int>(new Func<int>(() =>
{
    return Fac(40);   // 耗时任务
}));
// ContinueWith:等待上面的异步执行完毕之后才触发,避免阻塞当前线程
t.ContinueWith(t2 =>
{
    Console.WriteLine($"等待上面的任务完成,结果:{t2.Result}");
});
t.Start();
Console.WriteLine("我也要执行!!!!!!!!!!");   // 不等 t,先执行

Task.Run 快速开启 + WhenAny 竞速

csharp 复制代码
Task[] tasks = new Task[]
{
    Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("点外卖"); }),
    Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("吃外卖"); }),
    Task.Run(() => { Thread.Sleep(2000); Console.WriteLine("扔外卖"); })
};

// 任何一个异步执行完毕,就进入 continue(竞速)
Task.WhenAny(tasks).ContinueWith(t =>
{
    Console.WriteLine("任何一个异步执行完毕,就进入 continue");
});

// 另外两种:WhenAll 全部完成 / WaitAny 返回最先完成的索引
// int ind = Task.WaitAny(tasks);
// Task.WhenAll(tasks).ContinueWith(t => Console.WriteLine("全部干完了"));

WhenAny:谁先完成谁触发(竞速);WhenAll:全部完成才触发。


七、实战:递归遍历文件夹下的所有 .txt

把"递归"用在文件系统上------遇文件判断扩展名,遇文件夹就递归进入:

csharp 复制代码
private void FindTxtFiles(string folderPath)
{
    string[] filePath = Directory.GetFileSystemEntries(folderPath);
    foreach (var item in filePath)
    {
        if (File.Exists(item))                    // 是文件
        {
            if (Path.GetExtension(item) == ".txt")   // 是 txt
                Console.WriteLine(item);
        }
        else if (Directory.Exists(item))          // 是文件夹
        {
            // 核心:递归,进入下一层
            FindTxtFiles(item);
        }
    }
}

八、上篇小结

知识点 一句话
同步 vs 异步 同步顺序执行易卡顿,异步不阻塞当前线程
Thread 多请"人"干活,new Thread(()=>{}) 最简
跨线程 UI 必须 Invoke,否则报"线程间操作无效"
线程停止 Abort / Join / Suspend / Resume,出错用 ResetAbort
线程池 ThreadPool.QueueUserWorkItem,统一管理
ManualResetEvent Set() 发信号,WaitOne() 等待,控制任务顺序
Task 带返回值 + ContinueWith 链式,WhenAny/WhenAll
递归 遍历文件系统:文件处理、文件夹递归

下篇预告

多线程搞定了,那 ListView 文件管理器 是怎么实现"按名称/日期/类型/大小"自定义排序的?IComparer 接口怎么用?多个窗体怎么嵌进一个主界面?下篇《C# 自定义排序 IComparer + FileList 文件管理器实战》继续,点关注别错过!

参考:结合 C# 同步异步、多线程、线程池、Task 教学讲义整理的学习笔记。

相关推荐
Java后端的Ai之路1 小时前
01、Java并发编程:学习攻略如何才能学好并发编程?
java·开发语言·学习
垆边人似月.1 小时前
Ubuntu 配置 Samba 共享文件夹并映射到 Windows 完整指南
linux·windows·ubuntu
OxYGC2 小时前
[工程] PHP2Shell 漏洞解析:两条 PHP 内核 RCE,为什么应用层的防护全部失效
android·开发语言·php
小徐不徐说2 小时前
数据库存储基础:理解表结构、主键、事务与 BLOB
开发语言·数据库·c++·qt·程序设计
草莓熊Lotso2 小时前
【C++ 基于微服务的即时通讯系统】网关基建:cpp-httplib 与 WebSocketpp 从入门到实战
linux·开发语言·数据库·c++·redis·微服务·云原生
李纲明2 小时前
WordPress 媒体库上传失败或一直转圈:先核 PHP 上传限制与目录权限,再查安全插件与对象存储
运维·开发语言·安全·php·wordpress·媒体库·devdiy
吃一碗粉只付一碗钱10 小时前
12-给SPA友好的401和403响应
c#·状态模式
第九序言T10 小时前
Python 数据处理进阶(十八)批量处理同结构文件:一个文件夹一次跑完
开发语言·python·自动化·pandas·数据处理
龙亘川12 小时前
构建城市公共安全数字底座:亘川智城智慧安防系统全维度解读
java·大数据·开发语言·人工智能·信息可视化