题目从易到难:
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第一道
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//Task.Run 开辟后台线程,Sleep 不会阻塞 UI 主线程,窗口可正常操作
Task.Run(() =>
{
Thread.Sleep(3000);
MessageBox.Show("任务完成!");
});
}
}
}
一、核心知识点拆解
1. Task.Run() 开启线程池后台任务
- 作用:从系统线程池取出空闲线程,执行括号内的逻辑;
- 关键特性:不占用 UI 主线程,耗时操作放在这里不会造成窗口假死;
- 对比:如果直接在 button1_Click 里写
Thread.Sleep(3000),会阻塞 UI,3 秒内窗口无法拖动、点击。
2. Thread.Sleep(3000) 线程休眠
- 让当前所在线程暂停 3000 毫秒(3 秒);
- 这里代码在 Task 后台线程里执行,所以只阻塞后台线程,不影响界面;
- 注意:Sleep 是同步阻塞,会占用线程资源。
3. 跨线程弹窗特殊点(本题无报错原因)
正常情况下,子线程直接操作 Label、TextBox 等控件会报「跨线程操作无效」,但MessageBox.Show例外:
- MessageBox 底层内部自动切换到 UI 线程弹窗,子线程可以直接调用,不会报错;
- 拓展:如果把代码改成
label1.Text = "任务完成",会直接编译运行报错,必须用Invoke切回主线程。
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第二道
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
List<Task> tasks = new List<Task>();
tasks.Add(Task.Run (() =>Thread.Sleep(1000)));
tasks.Add(Task.Run(() => Thread.Sleep(2000)));
//ContinueWith 是 Task 的链式回调方法,前置任务执行完后,自动运行这里的代码。
Task.WhenAll(tasks).ContinueWith(v =>
{
MessageBox.Show("两个任务都完成了!!");
});
}
}
}
第二道代码完整知识总结
一、整体功能
创建 2 个并发后台任务:任务 1 休眠 1 秒、任务 2 休眠 2 秒;等两个任务全部执行完毕后,自动弹出提示框,全程不会阻塞 UI 窗口。
二、逐段核心知识点拆解
1. List<Task> tasks = new List<Task>();
- 作用:任务集合,批量存放多个
Task后台任务,方便统一等待管理; - 适用场景:多任务并发、需要批量等待全部完成时使用。
2. tasks.Add(Task.Run(() => Thread.Sleep(xxx)));
Task.Run():从线程池开启后台子线程,两个任务同时并发执行;Thread.Sleep(1000/2000):阻塞当前后台线程,模拟耗时操作;- 运行时序:任务 1 睡 1s、任务 2 睡 2s,总耗时由最长的 2 秒决定,不是 1+2=3 秒。
3. Task.WhenAll(tasks)
- 不会阻塞当前 UI 主线程,仅生成一个 "汇总等待任务";
- 只有集合内所有 Task 全部执行结束 ,
WhenAll才算完成; - 返回值为
Task对象,可以链式调用后续方法。
4. .ContinueWith(v => {}) 链式回调
- 执行时机:
WhenAll等待全部任务完成后,自动执行回调内代码; - 线程陷阱:回调内部代码运行在后台子线程,不是 UI 主线程;
- 特殊点:
MessageBox.Show可以在子线程直接调用不会报错; 如果换成label1.Text = "完成"会直接报跨线程操作无效 ,必须加Invoke调度到 UI 线程。
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第三道
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
Task taskA = Task.Run(() =>
{
Thread.Sleep(2000);
});
Task taskB = Task.Run(() =>
{
Thread.Sleep(4000);
});
await Task.WhenAll(taskA, taskB);
label1.Text = " await执行完后,代码自动回到UI主线程,可以直接操作label控件";
}
}
}
第三道代码完整知识总结
一、代码整体功能
点击按钮,同时开启两个后台并发任务(任务 A 休眠 2 秒、任务 B 休眠 4 秒); 异步等待两个任务全部执行完毕后,直接修改 Label 文本,全程窗口不会卡顿、可自由拖动操作。
二、逐段核心知识点拆解
1. private async void button1_Click
async关键字:方法内允许使用await等待异步操作;async void固定用于窗体控件点击事件(Click/Load 等),是 UI 事件专用写法;- 作用:
await等待期间释放 UI 主线程,界面不会假死。
2. Task.Run(()=>{ Thread.Sleep(2000); })
- 从线程池开辟独立后台子线程执行休眠;
- 两个任务并发同时运行,不是顺序执行;
- 总耗时由耗时最长的任务决定:只需要 4 秒,而非 2+4=6 秒;
Thread.Sleep仅阻塞当前后台线程,完全不影响 UI 界面。
3. Task.WhenAll(taskA, taskB)
- 接收一组 Task,代表 "等待全部任务完成";
- 纯异步逻辑,不会阻塞当前 UI 线程;
- 返回一个新 Task,配合
await实现异步等待。
4. await Task.WhenAll(taskA, taskB);
- 执行到
await时,立刻让出 UI 主线程,窗口可正常操作; - 等到 taskA、taskB 两个后台任务全部运行结束,代码会自动切回 UI 主线程继续往下执行;
- 核心优势:
await后的代码天然处于 UI 线程,直接读写 Label、TextBox 等控件,不需要写 Invoke 跨线程切换代码。
5. label1.Text = "xxx";
这里不会报 "跨线程操作无效",根本原因: 该行代码运行在await恢复后的 UI 主线程,和创建 label 的线程是同一个,允许直接操作控件。
三、运行时序完整流程
- 点击按钮,同时启动 taskA、taskB 两个后台线程;
- taskA 睡 2 秒先结束,taskB 继续休眠;
- 4 秒后 taskB 执行完毕,两个任务全部完成;
await等待结束,程序回到 UI 主线程;- 直接给 label1 赋值文字,界面同步刷新。
四、和上一道 ContinueWith 版本对比(核心优势)
表格
| 写法 | 任务完成后代码所在线程 | 操作控件是否需要 Invoke | 代码可读性 |
|---|---|---|---|
| 本题 async + await WhenAll | UI 主线程 | 不需要,直接操作 | 线性、易懂,无嵌套 |
| 第二道 WhenAll + ContinueWith | 后台子线程 | 必须加 Invoke,否则报错 | 回调嵌套,逻辑割裂 |
五、关键易错点提醒
async和await必须成对出现:方法不加 async,内部不能写 await;- 普通自定义业务方法不要写
async void,统一用async Task;仅 UI 事件可以 async void; Task.WhenAll是等待所有任务完成 ,与之对应Task.WhenAny是等待任意一个任务完成;- 并发任务总耗时 = 所有任务里最长的执行时间,不是累加和。
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第四题
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
Task<int> taskA = Task.Run(() =>//Task<int>:带返回值的任务类型,return 数字作为任务结果;
{
int sum = 0;
for (int i = 1; i <101; i++)
{
sum += i;
}
return sum;
});
Task<int> taskB = Task.Run(() =>
{
int sum = 0;
for(int i = 1;i < 201; i++)
{
sum += i;
}
return sum;
});
Task<int> taskC = Task.Run(() =>
{
int sum = 0;
for (int i = 1; i <301; i++)
{
sum += i;
}
return sum;
});
await Task.WhenAll(taskA, taskB, taskC);
int s1 = taskA.Result;//Task.Result:任务完成后读取内部返回值,await 等待完成后读取不会阻塞;
int s2 = taskB.Result;
int s3 = taskC.Result;
int total = s1 + s2 + s3;
MessageBox.Show($"1~100和:{s1}\n1~200和:{s2}\n1~300和:{s3}\n三者总和:{total}");
}
}
}
第四道代码完整知识总结
一、代码整体功能
点击按钮,同时开启 3 个带返回值的后台任务,分别计算 1~100、1~200、1~300 累加和; 异步等待三个任务全部计算完成,读取每个任务的返回结果,求和后弹窗分行展示所有数值,全程界面不会卡死。
二、逐段核心知识点拆解
1. async void button1_Click
UI 按钮点击事件专用异步写法:async允许内部写await,等待期间释放 UI 主线程,窗口可正常拖动操作。
2. Task<int> 带返回值的任务
- 普通无返回任务:
Task - 有返回值任务:
Task<T>,尖括号内int代表任务最终返回 int 类型数据 Task.Run(()=>{ ... return sum; }):后台线程计算,通过return把结果存入 Task 对象内部
3. 三个任务并发执行
taskA 算 1~100、taskB 算 1~200、taskC 算 1~300,三个任务同时并行运算,总耗时等于耗时最长的 taskC,不是三个循环时间相加。
4. await Task.WhenAll(taskA, taskB, taskC);
- 异步等待集合中全部任务执行完毕,不阻塞 UI 线程;
- 等待完成后代码自动切回 UI 主线程;
- 只有执行完这行,下面读取
.Result才是安全的。
5. taskA.Result 读取任务返回值
.Result作用:取出 Task 内部存储的 return 结果;- 关键前提:必须等任务执行完成后再读
Result,否则会阻塞当前线程; - 本题已经用
await WhenAll保证全部任务结束,此时读取Result不会阻塞线程; - 禁忌:不要在未 await 的 UI 线程直接调用
.Result,会卡死窗口。
6. 插值字符串 $"" + 换行符 \n
$插值字符串:{变量名}直接把数字嵌入文本,不用字符串拼接;\n:换行标记,弹窗内文字自动分行显示,展示更清晰。
三、运行完整流程
- 点击按钮,同时启动 3 条后台线程分别做累加计算;
- UI 线程释放,窗口可自由操作;
- 三个循环全部运算结束后,跳出 await;
- 依次读取三个任务的计算结果 s1、s2、s3;
- 计算三个数字总和 total;
- 弹出消息框,分行打印三段区间和、三者总和。
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第五道
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
Task task1 = Task.Run(() =>
{
Thread.Sleep(2000);
});
Task task2 = Task.Run(() =>
{
Thread.Sleep(3000);
});
Task task=await Task.WhenAny(task1, task2);//不会等全部任务,只要其中一个执行完成,立刻返回已完成的任务对象。
if (task == task1)
{
MessageBox.Show("成天了");
}
else
{
MessageBox.Show("其他");
}
}
}
}
第五道代码完整知识总结
一、整体功能
开启两个并发后台任务:task1 休眠 2 秒、task2 休眠 3 秒; 使用 Task.WhenAny 等待任意一个任务先完成,拿到最先结束的任务对象,判断弹出对应提示框;界面全程不会卡死。
二、逐行核心知识点拆解
1. async void button1_Click
UI 点击事件专用异步标记,方法内可使用await;等待过程释放 UI 主线程,窗口可正常拖动操作。
2. Task.Run(()=>Thread.Sleep(xxx))
- 开辟两条独立后台线程,两个任务同时并发执行;
- task1 只需要 2 秒就能执行完毕,task2 需要 3 秒;
Thread.Sleep仅阻塞后台线程,不影响界面。
3. Task task = await Task.WhenAny(task1, task2);
核心作用
WhenAny = 等待任意一个任务完成就立刻结束等待 ,不会像WhenAll一样等待全部任务。
- 执行逻辑:2 秒后 task1 先执行完毕,
WhenAny直接返回已经完成的 task1 对象; - 此时 task2 还在后台继续休眠,不会被终止,会跑完剩余 1 秒;
await等待完成后自动切回 UI 主线程,后续可直接操作控件。
和 WhenAll 核心对比
表格
| 方法 | 等待规则 | 返回值 |
|---|---|---|
| WhenAll | 等待所有任务全部结束 | 全部任务组成的 Task |
| WhenAny | 等待任意一个任务结束 | 最先完成的那一个 Task |
4. if (task == task1) 对象比对
WhenAny 返回的变量task存储的是最先执行完毕的任务引用 ,直接用==对比对象,判断是哪一个任务先完成:
- 本代码固定 task1 更快,永远走
MessageBox.Show("成天了");分支; - 如果交换 Sleep 时间(task1 睡 3s、task2 睡 2s),则会走 else 分支。
三、完整运行时序
- 点击按钮,task1、task2 同时启动;
- 2 秒后 task1 休眠结束,
WhenAny立刻停止等待,把 task1 赋值给变量 task; - 此时 task2 仍在后台继续运行,还要再休眠 1 秒才结束;
- 代码回到 UI 主线程,判断
task == task1成立,弹出提示框; - 弹窗弹出后,后台 task2 继续执行,直到 3 秒总时长结束。
四、关键易错点
- WhenAny 只会提前结束等待,不会终止剩下未完成的后台任务,剩余任务会继续跑完;
await WhenAny后代码在 UI 线程,弹窗、修改 label 无需 Invoke;WhenAny返回的是任务对象引用 ,可以直接用==区分是哪一个任务先完成;- 适用场景:多任务并行,只需要拿到第一个完成的结果(多接口抢响应、多文件快速读取等)。
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第六题
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Application.ThreadException += Application_ThreadException;
}
private void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
MessageBox.Show($"全局捕获异常:{e.Exception.Message}", "全局异常");
}
private async void button1_Click(object sender, EventArgs e)
{
// 任务1:内部捕获除零异常,不会向外抛出
Task<int> t1 = Task.Run(() =>
{
int res = 0;
try
{
int a = 100;
int b = 0;
res = a / b;
}
catch (DivideByZeroException)
{
MessageBox.Show("任务1出错:尝试除以零");
}
return res;
});
// 任务2:正常计算
Task<int> t2 = Task.Run(() =>
{
int a = 200;
int b = 4;
return a / b;
});
// 等待两个任务全部执行完毕
await Task.WhenAll(t1, t2);
// 输出正常任务结果
if (t2.IsCompleted && !t2.IsFaulted && !t2.IsCanceled)
{
MessageBox.Show($"正常任务2结果:{t2.Result}");
}
}
}
}
第六道代码完整知识总结
一、代码整体功能
- 注册全局 UI 线程异常捕获事件,用来抓取主线程未处理的报错;
- 开启两个带返回值的并发后台任务:
- t1:制造除零异常,任务内部
try-catch自行消化,异常不会向外扩散; - t2:正常整除运算,无错误;
- t1:制造除零异常,任务内部
await Task.WhenAll等待两个任务全部跑完;- 通过任务状态判断
t2是否执行成功,成功则弹出计算结果。
二、逐段核心知识点拆解
1. 全局异常注册:Application.ThreadException
csharp
运行
Application.ThreadException += Application_ThreadException;
- 作用:捕获UI 主线程 中没有被
try-catch捕获的异常; - 限制:无法捕获 Task 后台子线程内部的异常,子线程报错需要单独处理;
- 回调参数
ThreadExceptionEventArgs e:e.Exception拿到异常对象,可读取错误信息。
2. Task.Run 后台任务内局部 try-catch(t1)
csharp
运行
Task<int> t1 = Task.Run(() =>
{
int res = 0;
try
{
int a = 100;
int b = 0;
res = a / b; // 除零异常 DivideByZeroException
}
catch (DivideByZeroException)
{
MessageBox.Show("任务1出错:尝试除以零");
}
return res;
});
a / b,除数为 0,会触发除零异常;- 异常在后台线程内部直接捕获,不会存储到 Task 的异常集合里;
- t1 最终状态:
IsCompleted=true、IsFaulted=false,对外表现为正常完成,返回默认值 0; - 全局
Application.ThreadException完全感知不到这个错误。
3. 正常无报错任务 t2
简单整除运算 200 / 4 = 50,无任何异常,执行状态完全正常。
4. await Task.WhenAll(t1, t2);
异步等待两个并发任务全部执行完毕,等待期间不阻塞 UI 窗口; 因为 t1 内部已经消化异常,WhenAll 不会向外抛出聚合异常AggregateException。
5. Task 任务状态判断
csharp
运行
if (t2.IsCompleted && !t2.IsFaulted && !t2.IsCanceled)
Task 自带 3 个核心状态属性:
IsCompleted:true = 任务已经执行完毕(无论成功 / 出错 / 取消);IsFaulted:true = 任务内部发生未捕获异常,执行失败;IsCanceled:true = 任务被手动取消; 本行判断含义:任务执行完成、无报错、未取消 ,代表运算完全正常,安全读取.Result。
三、完整运行流程
- 窗体构造函数注册全局 UI 异常捕获;
- 点击按钮,同时启动 t1、t2 两个后台线程;
- t1 执行除法触发除零异常,内部 catch 捕获,弹出提示,返回 0;
- t2 正常计算 200/4=50,无报错;
WhenAll等待两个任务全部结束;- 判断 t2 状态正常,弹窗输出结果
50; - 全程全局异常事件不会触发,因为错误在子线程内部被处理。
四、关键拓展对比:子线程异常两种处理方式
方式 1:任务内部 catch(本题写法)
异常本地消化,Task 标记为正常完成,WhenAll不抛异常,全局捕获无效。
方式 2:任务不 catch,异常向外抛出
后台报错会存入 Task,await WhenAll时抛出AggregateException,需要在外层用try-catch包裹await才能捕获,全局 UI 异常事件依旧抓不到。 示例:
csharp
运行
// 任务不处理异常
Task<int> t1 = Task.Run(() =>
{
int a = 100, b = 0;
return a / b;
});
// 外层捕获聚合异常
try
{
await Task.WhenAll(t1, t2);
}
catch (AggregateException ex)
{
MessageBox.Show($"后台任务异常:{ex.InnerException.Message}");
}
五、核心易错点总结
Application.ThreadException只管UI 主线程,后台 Task 线程异常无法捕获;- 后台任务内部
try-catch会阻断异常向外传递,任务不会标记为 Faulted; - 读取
Task.Result前建议先判断IsFaulted,避免读取失败任务时报错; WhenAll只有当存在未捕获异常的任务时,才会抛出聚合异常;内部消化异常后不会报错。
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第七题
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
Task task1 = Task.Run(async () =>
{
await Task.Delay(3000);//等待结束后,代码自动切回原来的 UI 主线程,
//Task.Delay是.NET 内置异步延时方法,返回一个 Task,专门用来等待指定时间,全程不阻塞线程。
});
Task task2 = Task.Run(async () =>
{
await Task.Delay(2300);
});
Task task3 = Task.Run(async () =>
{
await Task.Delay(4500);
});
try
{
await Task.WhenAll(task1, task2);
label1.Text = "素材处理完成";
await Task.WhenAll(task1, task2, task3);
label1.Text = "全部素材处理完成";
}
catch (AggregateException ex)//存储一组多个异常 和Task.WhenAll 多并发任务一起使用
{
string err = "任务发生异常:\n";
foreach (var item in ex.InnerExceptions)
{
err += $".{item.Message}\n";
}
MessageBox.Show(err,"异常提醒");
}
}
}
}
第七题 完整知识点总结
一、代码整体功能
并发开启 3 个异步延时后台任务: task1 延时 3000ms、task2 延时 2300ms、task3 延时 4500ms;
- 先等待 task1、task2 全部完成,更新 Label 提示「素材处理完成」;
- 再等待 task1、task2、task3 全部完成,更新 Label 提示「全部素材处理完成」;
- 外层
try-catch(AggregateException)统一捕获多个并发任务抛出的所有异常。
二、逐段核心知识点拆解
1. Task.Run(async () => { await Task.Delay(xxx); })
async委托:Task.Run 内部可以写异步代码,搭配await;Task.Delay(毫秒):异步延时 API- 和
Thread.Sleep本质区别:不阻塞线程,线程释放给系统复用; Thread.Sleep是同步阻塞,占用线程;Task.Delay纯异步等待,无资源浪费;
- 和
- 误区纠正注释:
await Task.Delay是在后台子线程 内等待,不会切回 UI 主线程;只有按钮 Click 里外层的await Task.WhenAll执行完毕后,才会切回 UI 主线程。
2. 两段 await Task.WhenAll 执行逻辑
csharp
运行
await Task.WhenAll(task1, task2);
label1.Text = "素材处理完成";
await Task.WhenAll(task1, task2, task3);
label1.Text = "全部素材处理完成";
- 第一段
WhenAll(task1,task2)task2 耗时 2.3 秒、task1 耗时 3 秒,3 秒后两个任务全部结束,立刻给 label 赋值; 此时 task3 仍在后台继续倒计时,还没结束。 - 第二段
WhenAll(task1,task2,task3)task1、task2 早已执行完毕,只需等待剩余的 task3(4.5 秒总时长); 4.5 秒全部任务结束,更新最终提示文字。 - 关键:任务一旦执行完成,会永久处于完成状态,多次 WhenAll 等待不会重复执行。
3. AggregateException 聚合异常
- 适用场景:
WhenAll等待多个并发任务,若多个任务同时报错,所有异常会打包存入AggregateException; ex.InnerExceptions:集合,存放每一个后台任务抛出的原始异常;- 遍历循环可以取出所有任务的错误信息,统一弹窗展示。
4. 异步事件 async void button1_Click
所有await执行完成后,代码自动回到 UI 主线程,直接操作label1.Text无需Invoke,不会报跨线程错误。
三、完整时间执行时序
- 点击按钮,task1、task2、task3 同时启动;
- 2.3 秒:task2 延时结束;
- 3 秒:task1 延时结束,
await WhenAll(task1,task2)放行; UI 线程更新 label 为「素材处理完成」; - 继续等待 task3,到 4.5 秒时 task3 结束;
await WhenAll(task1,task2,task3)放行; UI 线程更新 label 为「全部素材处理完成」。
四、核心对比:Task.Delay vs Thread.Sleep
表格
| 方法 | 类型 | 是否阻塞线程 | 适用场景 |
|---|---|---|---|
| Task.Delay | 异步 await | 不阻塞,释放线程 | Task 异步并发、UI 项目 |
| Thread.Sleep | 同步阻塞 | 占用当前线程 | 老式同步后台代码,不推荐 UI |
五、易错点说明
Task.Delay在 Task.Run 内部执行,不会切回 UI 线程;只有外层 Click 的 await 恢复后才在 UI 线程;AggregateException只能捕获未被内部 try-catch 处理的后台异常;任务内部捕获的错误不会进入聚合异常;- 已完成的 Task 重复 WhenAll 不会重新执行,只会直接放行;
- 多段顺序
await WhenAll会分段等待,可实现分阶段进度提示。
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 第八题
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
SemaphoreSlim slim = new SemaphoreSlim(3);//SemaphoreSlim:轻量级信号量,用来控制同一时间最多能同时运行多少个任务,实现并发限流。
Task[] tasks = new Task[10];//创建长度为 10 的数组,
for (int i = 0; i < 10; i++)
{
int index = i;
tasks[i] = Task.Run(async () =>
{
await slim.WaitAsync();//SemaphoreSlim 的异步等待方法,作用:向信号量申请一个运行许可
try
{
MessageBox.Show($"第{index+1}个任务开始执行");
await Task.Delay(1000);
MessageBox.Show($"第{index + 1}个任务执行完成!!!");
}
finally//finally 里的代码也一定会执行
{
slim.Release();//slim.Release():用完名额还给信号量,释放名额。
}
});
}
await Task.WhenAll(tasks);
MessageBox.Show("10个任务执行结束");
}
}
}
第八题完整知识点总结
一、整体功能
创建 10 个并发后台任务,通过 SemaphoreSlim(3) 限流:同一时间最多只能同时运行 3 个任务,任务执行完毕归还名额,全部 10 个任务跑完后弹窗提示结束。
二、逐段核心知识点拆解
1. SemaphoreSlim slim = new SemaphoreSlim(3);
SemaphoreSlim:轻量级异步信号量,用于控制并发数量(限流);- 构造参数
3= 许可数量,代表最多同时放行 3 个线程 / 任务; - 内部维护一个计数器,初始值 = 3。
2. Task[] tasks = new Task[10];
定义长度为 10 的 Task 数组,一次性存储 10 个待执行后台任务。
3. int index = i; 循环闭包经典处理
for 循环内直接使用i会出现变量捕获错乱,单独用index接收当前循环值,每个任务能拿到自己正确的序号。
4. await slim.WaitAsync();
- 异步申请执行许可;
- 信号量计数器 > 0:直接拿到名额,计数器 - 1,任务继续执行;
- 信号量计数器 = 0:任务阻塞等待,直到有其他任务 Release 释放名额;
- 异步等待不卡死 UI 线程。
5. try-finally 结构
csharp
运行
try
{
// 任务业务逻辑
}
finally
{
slim.Release();
}
finally无论 try 内代码正常执行、还是抛出异常,一定会执行;slim.Release():归还 1 个许可,信号量计数器 + 1,给排队的其他任务腾出名额;- 关键规范:Release 必须写在 finally,保证名额不会丢失、死锁。
6. 任务内部逻辑
每个任务弹窗提示开始,延时 1 秒模拟耗时操作,再弹窗提示完成。
7. await Task.WhenAll(tasks);
异步等待数组中 10 个任务全部执行完毕,之后弹出总完成提示。
三、完整运行流程
- 一次性创建 10 个任务,全部进入就绪状态;
- 前 3 个任务调用
WaitAsync直接拿到许可,同时运行;计数器变为 0; - 第 4~10 个任务卡在
WaitAsync排队; - 任意一个任务延时结束,进入 finally 执行
Release,计数器 + 1; - 排队中的下一个任务立刻拿到许可开始运行;
- 循环往复,始终保持最多 3 个任务同时执行;
- 10 个任务全部执行完后,弹窗 "10 个任务执行结束"。
四、核心作用对比
SemaphoreSlim(1):等价异步线程锁,同一时间只允许 1 个任务执行;SemaphoreSlim(N):并发限流,控制同时运行任务上限 N 个;- lock 同步锁:不支持 await 异步场景;异步限流统一用 SemaphoreSlim。
五、关键易错点
Release()必须放在finally,否则任务中途报错会丢失许可,信号量卡死;WaitAsync()搭配await,同步场景可用Wait();- for 循环中必须临时变量承接 i,否则所有任务拿到的 i 都是循环最终值;
- 信号量初始值就是最大并发数,数值越大同时运行任务越多。
六、一句话记忆
SemaphoreSlim 控制并发上限,WaitAsync 抢名额,Release 归还名额,finally 保证一定释放。