WinForm练习知识补充(多线程)

题目从易到难:

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() 开启线程池后台任务

  1. 作用:从系统线程池取出空闲线程,执行括号内的逻辑;
  2. 关键特性:不占用 UI 主线程,耗时操作放在这里不会造成窗口假死;
  3. 对比:如果直接在 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)

  1. 不会阻塞当前 UI 主线程,仅生成一个 "汇总等待任务";
  2. 只有集合内所有 Task 全部执行结束WhenAll才算完成;
  3. 返回值为Task对象,可以链式调用后续方法。

4. .ContinueWith(v => {}) 链式回调

  1. 执行时机:WhenAll等待全部任务完成后,自动执行回调内代码;
  2. 线程陷阱:回调内部代码运行在后台子线程,不是 UI 主线程;
  3. 特殊点: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

  1. async 关键字:方法内允许使用 await 等待异步操作;
  2. async void 固定用于窗体控件点击事件(Click/Load 等),是 UI 事件专用写法;
  3. 作用:await 等待期间释放 UI 主线程,界面不会假死。

2. Task.Run(()=>{ Thread.Sleep(2000); })

  1. 从线程池开辟独立后台子线程执行休眠;
  2. 两个任务并发同时运行,不是顺序执行;
  3. 总耗时由耗时最长的任务决定:只需要 4 秒,而非 2+4=6 秒;
  4. Thread.Sleep 仅阻塞当前后台线程,完全不影响 UI 界面。

3. Task.WhenAll(taskA, taskB)

  • 接收一组 Task,代表 "等待全部任务完成";
  • 纯异步逻辑,不会阻塞当前 UI 线程;
  • 返回一个新 Task,配合await实现异步等待。

4. await Task.WhenAll(taskA, taskB);

  1. 执行到await时,立刻让出 UI 主线程,窗口可正常操作;
  2. 等到 taskA、taskB 两个后台任务全部运行结束,代码会自动切回 UI 主线程继续往下执行;
  3. 核心优势:await后的代码天然处于 UI 线程,直接读写 Label、TextBox 等控件,不需要写 Invoke 跨线程切换代码

5. label1.Text = "xxx";

这里不会报 "跨线程操作无效",根本原因: 该行代码运行在await恢复后的 UI 主线程,和创建 label 的线程是同一个,允许直接操作控件。

三、运行时序完整流程

  1. 点击按钮,同时启动 taskA、taskB 两个后台线程;
  2. taskA 睡 2 秒先结束,taskB 继续休眠;
  3. 4 秒后 taskB 执行完毕,两个任务全部完成;
  4. await等待结束,程序回到 UI 主线程;
  5. 直接给 label1 赋值文字,界面同步刷新。

四、和上一道 ContinueWith 版本对比(核心优势)

表格

写法 任务完成后代码所在线程 操作控件是否需要 Invoke 代码可读性
本题 async + await WhenAll UI 主线程 不需要,直接操作 线性、易懂,无嵌套
第二道 WhenAll + ContinueWith 后台子线程 必须加 Invoke,否则报错 回调嵌套,逻辑割裂

五、关键易错点提醒

  1. asyncawait 必须成对出现:方法不加 async,内部不能写 await;
  2. 普通自定义业务方法不要写async void,统一用async Task;仅 UI 事件可以 async void;
  3. Task.WhenAll 是等待所有任务完成 ,与之对应 Task.WhenAny 是等待任意一个任务完成
  4. 并发任务总耗时 = 所有任务里最长的执行时间,不是累加和。
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);

  1. 异步等待集合中全部任务执行完毕,不阻塞 UI 线程;
  2. 等待完成后代码自动切回 UI 主线程;
  3. 只有执行完这行,下面读取.Result才是安全的。

5. taskA.Result 读取任务返回值

  • .Result作用:取出 Task 内部存储的 return 结果;
  • 关键前提:必须等任务执行完成后再读Result,否则会阻塞当前线程;
  • 本题已经用await WhenAll保证全部任务结束,此时读取Result不会阻塞线程;
  • 禁忌:不要在未 await 的 UI 线程直接调用.Result,会卡死窗口。

6. 插值字符串 $"" + 换行符 \n

  • $插值字符串:{变量名}直接把数字嵌入文本,不用字符串拼接;
  • \n:换行标记,弹窗内文字自动分行显示,展示更清晰。

三、运行完整流程

  1. 点击按钮,同时启动 3 条后台线程分别做累加计算;
  2. UI 线程释放,窗口可自由操作;
  3. 三个循环全部运算结束后,跳出 await;
  4. 依次读取三个任务的计算结果 s1、s2、s3;
  5. 计算三个数字总和 total;
  6. 弹出消息框,分行打印三段区间和、三者总和。
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一样等待全部任务。

  1. 执行逻辑:2 秒后 task1 先执行完毕,WhenAny 直接返回已经完成的 task1 对象
  2. 此时 task2 还在后台继续休眠,不会被终止,会跑完剩余 1 秒;
  3. await 等待完成后自动切回 UI 主线程,后续可直接操作控件。
和 WhenAll 核心对比

表格

方法 等待规则 返回值
WhenAll 等待所有任务全部结束 全部任务组成的 Task
WhenAny 等待任意一个任务结束 最先完成的那一个 Task

4. if (task == task1) 对象比对

WhenAny 返回的变量task存储的是最先执行完毕的任务引用 ,直接用==对比对象,判断是哪一个任务先完成:

  • 本代码固定 task1 更快,永远走MessageBox.Show("成天了");分支;
  • 如果交换 Sleep 时间(task1 睡 3s、task2 睡 2s),则会走 else 分支。

三、完整运行时序

  1. 点击按钮,task1、task2 同时启动;
  2. 2 秒后 task1 休眠结束,WhenAny立刻停止等待,把 task1 赋值给变量 task;
  3. 此时 task2 仍在后台继续运行,还要再休眠 1 秒才结束;
  4. 代码回到 UI 主线程,判断task == task1成立,弹出提示框;
  5. 弹窗弹出后,后台 task2 继续执行,直到 3 秒总时长结束。

四、关键易错点

  1. WhenAny 只会提前结束等待,不会终止剩下未完成的后台任务,剩余任务会继续跑完;
  2. await WhenAny 后代码在 UI 线程,弹窗、修改 label 无需 Invoke;
  3. WhenAny 返回的是任务对象引用 ,可以直接用==区分是哪一个任务先完成;
  4. 适用场景:多任务并行,只需要拿到第一个完成的结果(多接口抢响应、多文件快速读取等)。
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}");
            }
        }
    }
}

第六道代码完整知识总结

一、代码整体功能

  1. 注册全局 UI 线程异常捕获事件,用来抓取主线程未处理的报错;
  2. 开启两个带返回值的并发后台任务:
    • t1:制造除零异常,任务内部try-catch自行消化,异常不会向外扩散;
    • t2:正常整除运算,无错误;
  3. await Task.WhenAll等待两个任务全部跑完;
  4. 通过任务状态判断t2是否执行成功,成功则弹出计算结果。

二、逐段核心知识点拆解

1. 全局异常注册:Application.ThreadException

csharp

运行

复制代码
Application.ThreadException += Application_ThreadException;
  • 作用:捕获UI 主线程 中没有被try-catch捕获的异常;
  • 限制:无法捕获 Task 后台子线程内部的异常,子线程报错需要单独处理;
  • 回调参数ThreadExceptionEventArgs ee.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;
});
  1. a / b,除数为 0,会触发除零异常
  2. 异常在后台线程内部直接捕获,不会存储到 Task 的异常集合里;
  3. t1 最终状态:IsCompleted=true、IsFaulted=false,对外表现为正常完成,返回默认值 0;
  4. 全局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 个核心状态属性:

  1. IsCompleted:true = 任务已经执行完毕(无论成功 / 出错 / 取消);
  2. IsFaulted:true = 任务内部发生未捕获异常,执行失败;
  3. IsCanceled:true = 任务被手动取消; 本行判断含义:任务执行完成、无报错、未取消 ,代表运算完全正常,安全读取.Result

三、完整运行流程

  1. 窗体构造函数注册全局 UI 异常捕获;
  2. 点击按钮,同时启动 t1、t2 两个后台线程;
  3. t1 执行除法触发除零异常,内部 catch 捕获,弹出提示,返回 0;
  4. t2 正常计算 200/4=50,无报错;
  5. WhenAll等待两个任务全部结束;
  6. 判断 t2 状态正常,弹窗输出结果50
  7. 全程全局异常事件不会触发,因为错误在子线程内部被处理。

四、关键拓展对比:子线程异常两种处理方式

方式 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}");
}

五、核心易错点总结

  1. Application.ThreadException只管UI 主线程,后台 Task 线程异常无法捕获;
  2. 后台任务内部try-catch会阻断异常向外传递,任务不会标记为 Faulted;
  3. 读取Task.Result前建议先判断IsFaulted,避免读取失败任务时报错;
  4. 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;

  1. 先等待 task1、task2 全部完成,更新 Label 提示「素材处理完成」;
  2. 再等待 task1、task2、task3 全部完成,更新 Label 提示「全部素材处理完成」;
  3. 外层try-catch(AggregateException)统一捕获多个并发任务抛出的所有异常。

二、逐段核心知识点拆解

1. Task.Run(async () => { await Task.Delay(xxx); })

  1. async 委托:Task.Run 内部可以写异步代码,搭配 await
  2. Task.Delay(毫秒):异步延时 API
    • Thread.Sleep 本质区别:不阻塞线程,线程释放给系统复用;
    • Thread.Sleep 是同步阻塞,占用线程;Task.Delay 纯异步等待,无资源浪费;
  3. 误区纠正注释: 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 = "全部素材处理完成";
  1. 第一段 WhenAll(task1,task2) task2 耗时 2.3 秒、task1 耗时 3 秒,3 秒后两个任务全部结束,立刻给 label 赋值; 此时 task3 仍在后台继续倒计时,还没结束。
  2. 第二段 WhenAll(task1,task2,task3) task1、task2 早已执行完毕,只需等待剩余的 task3(4.5 秒总时长); 4.5 秒全部任务结束,更新最终提示文字。
  3. 关键:任务一旦执行完成,会永久处于完成状态,多次 WhenAll 等待不会重复执行。

3. AggregateException 聚合异常

  1. 适用场景:WhenAll 等待多个并发任务,若多个任务同时报错,所有异常会打包存入AggregateException
  2. ex.InnerExceptions:集合,存放每一个后台任务抛出的原始异常;
  3. 遍历循环可以取出所有任务的错误信息,统一弹窗展示。

4. 异步事件 async void button1_Click

所有await执行完成后,代码自动回到 UI 主线程,直接操作label1.Text无需Invoke,不会报跨线程错误。

三、完整时间执行时序

  1. 点击按钮,task1、task2、task3 同时启动;
  2. 2.3 秒:task2 延时结束;
  3. 3 秒:task1 延时结束,await WhenAll(task1,task2)放行; UI 线程更新 label 为「素材处理完成」;
  4. 继续等待 task3,到 4.5 秒时 task3 结束; await WhenAll(task1,task2,task3)放行; UI 线程更新 label 为「全部素材处理完成」。

四、核心对比:Task.Delay vs Thread.Sleep

表格

方法 类型 是否阻塞线程 适用场景
Task.Delay 异步 await 不阻塞,释放线程 Task 异步并发、UI 项目
Thread.Sleep 同步阻塞 占用当前线程 老式同步后台代码,不推荐 UI

五、易错点说明

  1. Task.Delay 在 Task.Run 内部执行,不会切回 UI 线程;只有外层 Click 的 await 恢复后才在 UI 线程;
  2. AggregateException 只能捕获未被内部 try-catch 处理的后台异常;任务内部捕获的错误不会进入聚合异常;
  3. 已完成的 Task 重复 WhenAll 不会重新执行,只会直接放行;
  4. 多段顺序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();
}
  1. finally 无论 try 内代码正常执行、还是抛出异常,一定会执行
  2. slim.Release():归还 1 个许可,信号量计数器 + 1,给排队的其他任务腾出名额;
  3. 关键规范:Release 必须写在 finally,保证名额不会丢失、死锁。

6. 任务内部逻辑

每个任务弹窗提示开始,延时 1 秒模拟耗时操作,再弹窗提示完成。

7. await Task.WhenAll(tasks);

异步等待数组中 10 个任务全部执行完毕,之后弹出总完成提示。

三、完整运行流程

  1. 一次性创建 10 个任务,全部进入就绪状态;
  2. 前 3 个任务调用WaitAsync直接拿到许可,同时运行;计数器变为 0;
  3. 第 4~10 个任务卡在WaitAsync排队;
  4. 任意一个任务延时结束,进入 finally 执行Release,计数器 + 1;
  5. 排队中的下一个任务立刻拿到许可开始运行;
  6. 循环往复,始终保持最多 3 个任务同时执行;
  7. 10 个任务全部执行完后,弹窗 "10 个任务执行结束"。

四、核心作用对比

  1. SemaphoreSlim(1):等价异步线程锁,同一时间只允许 1 个任务执行;
  2. SemaphoreSlim(N):并发限流,控制同时运行任务上限 N 个;
  3. lock 同步锁:不支持 await 异步场景;异步限流统一用 SemaphoreSlim。

五、关键易错点

  1. Release() 必须放在finally,否则任务中途报错会丢失许可,信号量卡死;
  2. WaitAsync() 搭配await,同步场景可用Wait()
  3. for 循环中必须临时变量承接 i,否则所有任务拿到的 i 都是循环最终值;
  4. 信号量初始值就是最大并发数,数值越大同时运行任务越多。

六、一句话记忆

SemaphoreSlim 控制并发上限,WaitAsync 抢名额,Release 归还名额,finally 保证一定释放。

相关推荐
code bean9 小时前
【C#】 `Channel<T>` 深度解析:生产者-消费者模式的现代解法
数据结构·c#
吴可可12313 小时前
C# CAD二次开发:合并首尾重合多段线
c#
EIP低代码平台14 小时前
EIP低代码平台 - 应用管理 - 表单设计
低代码·c#·权限·工作流·netcore
czhc114007566314 小时前
726:zoffset
c#
向夏威夷 梦断明暄17 小时前
C# 弃元模式:从语法糖到性能利器的深度解析
服务器·数据库·c#
rick97718 小时前
C# × Python 互操不再难:DotNetPy 让两大生态真正融合
c#
ellis197019 小时前
C#/Unity清理非托管资源
unity·c#
吴可可1231 天前
C#用OpenCVSharp提取轮廓生成CAD多段线
c#
玖玥拾1 天前
Unity 3D 笔记(十四)Unity/C# Socket 网络笔记3
服务器·网络·unity·c#
玖玥拾2 天前
Unity 3D 笔记(十七)Unity/C# Socket 网络笔记6
服务器·网络·unity·c#