WinForm练习知识补充(多线程)
题目从易到难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)会阻塞 UI3 秒内窗口无法拖动、点击。2.Thread.Sleep(3000)线程休眠让当前所在线程暂停 3000 毫秒3 秒这里代码在 Task 后台线程里执行所以只阻塞后台线程不影响界面注意Sleep 是同步阻塞会占用线程资源。3. 跨线程弹窗特殊点本题无报错原因正常情况下子线程直接操作 Label、TextBox 等控件会报「跨线程操作无效」但MessageBox.Show例外MessageBox 底层内部自动切换到 UI 线程弹窗子线程可以直接调用不会报错拓展如果把代码改成label1.Text 任务完成会直接编译运行报错必须用Invoke切回主线程。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) { ListTask tasks new ListTask(); 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.ListTask tasks new ListTask();作用任务集合批量存放多个Task后台任务方便统一等待管理适用场景多任务并发、需要批量等待全部完成时使用。2.tasks.Add(Task.Run(() Thread.Sleep(xxx)));Task.Run()从线程池开启后台子线程两个任务同时并发执行Thread.Sleep(1000/2000)阻塞当前后台线程模拟耗时操作运行时序任务 1 睡 1s、任务 2 睡 2s总耗时由最长的 2 秒决定不是 123 秒。3.Task.WhenAll(tasks)不会阻塞当前 UI 主线程仅生成一个 “汇总等待任务”只有集合内所有 Task 全部执行结束WhenAll才算完成返回值为Task对象可以链式调用后续方法。4..ContinueWith(v {})链式回调执行时机WhenAll等待全部任务完成后自动执行回调内代码线程陷阱回调内部代码运行在后台子线程不是 UI 主线程特殊点MessageBox.Show可以在子线程直接调用不会报错 如果换成label1.Text 完成会直接报跨线程操作无效必须加Invoke调度到 UI 线程。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_Clickasync关键字方法内允许使用await等待异步操作async void固定用于窗体控件点击事件Click/Load 等是 UI 事件专用写法作用await等待期间释放 UI 主线程界面不会假死。2.Task.Run((){ Thread.Sleep(2000); })从线程池开辟独立后台子线程执行休眠两个任务并发同时运行不是顺序执行总耗时由耗时最长的任务决定只需要 4 秒而非 246 秒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 WhenAllUI 主线程不需要直接操作线性、易懂无嵌套第二道 WhenAll ContinueWith后台子线程必须加 Invoke否则报错回调嵌套逻辑割裂五、关键易错点提醒async和await必须成对出现方法不加 async内部不能写 await普通自定义业务方法不要写async void统一用async Task仅 UI 事件可以 async voidTask.WhenAll是等待所有任务完成与之对应Task.WhenAny是等待任意一个任务完成并发任务总耗时 所有任务里最长的执行时间不是累加和。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) { Taskint taskA Task.Run(() //Taskint带返回值的任务类型return 数字作为任务结果 { int sum 0; for (int i 1; i 101; i) { sum i; } return sum; }); Taskint taskB Task.Run(() { int sum 0; for(int i 1;i 201; i) { sum i; } return sum; }); Taskint 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_ClickUI 按钮点击事件专用异步写法async允许内部写await等待期间释放 UI 主线程窗口可正常拖动操作。2.Taskint带返回值的任务普通无返回任务Task有返回值任务TaskT尖括号内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弹出消息框分行打印三段区间和、三者总和。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 taskawait Task.WhenAny(task1, task2);//不会等全部任务只要其中一个执行完成立刻返回已完成的任务对象。 if (task task1) { MessageBox.Show(成天了); } else { MessageBox.Show(其他); } } } }第五道代码完整知识总结一、整体功能开启两个并发后台任务task1 休眠 2 秒、task2 休眠 3 秒 使用Task.WhenAny等待任意一个任务先完成拿到最先结束的任务对象判断弹出对应提示框界面全程不会卡死。二、逐行核心知识点拆解1.async void button1_ClickUI 点击事件专用异步标记方法内可使用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等待所有任务全部结束全部任务组成的 TaskWhenAny等待任意一个任务结束最先完成的那一个 Task4.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 无需 InvokeWhenAny返回的是任务对象引用可以直接用区分是哪一个任务先完成适用场景多任务并行只需要拿到第一个完成的结果多接口抢响应、多文件快速读取等。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内部捕获除零异常不会向外抛出 Taskint t1 Task.Run(() { int res 0; try { int a 100; int b 0; res a / b; } catch (DivideByZeroException) { MessageBox.Show(任务1出错尝试除以零); } return res; }); // 任务2正常计算 Taskint 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正常整除运算无错误await Task.WhenAll等待两个任务全部跑完通过任务状态判断t2是否执行成功成功则弹出计算结果。二、逐段核心知识点拆解1. 全局异常注册Application.ThreadExceptioncsharp运行Application.ThreadException Application_ThreadException;作用捕获UI 主线程中没有被try-catch捕获的异常限制无法捕获 Task 后台子线程内部的异常子线程报错需要单独处理回调参数ThreadExceptionEventArgs ee.Exception拿到异常对象可读取错误信息。2. Task.Run 后台任务内局部 try-catcht1csharp运行Taskint 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 最终状态IsCompletedtrue、IsFaultedfalse对外表现为正常完成返回默认值 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 个核心状态属性IsCompletedtrue 任务已经执行完毕无论成功 / 出错 / 取消IsFaultedtrue 任务内部发生未捕获异常执行失败IsCanceledtrue 任务被手动取消 本行判断含义任务执行完成、无报错、未取消代表运算完全正常安全读取.Result。三、完整运行流程窗体构造函数注册全局 UI 异常捕获点击按钮同时启动 t1、t2 两个后台线程t1 执行除法触发除零异常内部 catch 捕获弹出提示返回 0t2 正常计算 200/450无报错WhenAll等待两个任务全部结束判断 t2 状态正常弹窗输出结果50全程全局异常事件不会触发因为错误在子线程内部被处理。四、关键拓展对比子线程异常两种处理方式方式 1任务内部 catch本题写法异常本地消化Task 标记为正常完成WhenAll不抛异常全局捕获无效。方式 2任务不 catch异常向外抛出后台报错会存入 Taskawait WhenAll时抛出AggregateException需要在外层用try-catch包裹await才能捕获全局 UI 异常事件依旧抓不到。 示例csharp运行// 任务不处理异常 Taskint 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只有当存在未捕获异常的任务时才会抛出聚合异常内部消化异常后不会报错。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 内部可以写异步代码搭配awaitTask.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 早已执行完毕只需等待剩余的 task34.5 秒总时长 4.5 秒全部任务结束更新最终提示文字。关键任务一旦执行完成会永久处于完成状态多次 WhenAll 等待不会重复执行。3.AggregateException聚合异常适用场景WhenAll等待多个并发任务若多个任务同时报错所有异常会打包存入AggregateExceptionex.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会分段等待可实现分阶段进度提示。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($第{index1}个任务开始执行); 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 保证一定释放。