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 教学讲义整理的学习笔记。