Asp.Net Core 轻松学-多线程之Task快速上手
前言
task是从 .net framework 4 开始引入的一项基于队列的异步任务(tap)模式,从 .net framework 4.5 开始,任何使用 async/await 进行修饰的方法,都会被认为是一个异步方法;实际上,这些异步方法都是基于队列的线程任务,从你开始使用 task 去运行一段代码的时候,实际上就相当于开启了一个线程,默认情况下,这个线程数由线程池 threadpool 进行管理的。
1. task 的使用方法
task 的使用用方法非常简单,一行代码就可以开始一个异步任务
1.1 最简单的使用方式
static void easytask() { // 执行一个无返回值的任务 task.run(() => { console.writeline("runing..."); }); // 执行一个返回 int 类型结果的任务 task.run<int>(() => { return new random().next(); }); // 声明一个任务,仅声明,不执行 task t = new task(() => { console.writeline(""); }); }
上面的代码看起来非常简单,只需要一行代码就完成了一个异步任务线程,先不要去深究其背后的原理,对于新手来说,先解决能用,再去了解为什么可以这样使用,不然,一开始就失去了学习的信心
2.1 使用 taskfactory 工厂开始异步任务
static void factory() { list<task<int>> tasks = new list<task<int>>(); taskfactory factory = new taskfactory(); tasks.add(factory.startnew<int>(() => { return 1; })); tasks.add(factory.startnew<int>(() => { return 2; })); foreach (var t in tasks) { console.writeline("task:{0}", t.result); } }
上面的代码使用 taskfactory 创建并运行了两个异步任务,同时把这两个任务加入了任务列表 tasks 中,然后立即迭代此 tasks 获取异步任务的执行结果,使用 taskfactory 工厂类,可以创建一组人物,然后依次执行它们
2.3 执行上面的代码,输出结果如下
3. 处理 task 中的异常
异步任务中发生异常会导致任务抛出 taskcancelexception 的异常,仅表示任务退出,程序应当捕获该异常;然后,立即调用 task 进行状态判断,获取内部异常
3.1 模拟抛出异常
static void simpletask() { var task = task.run(() => { console.writeline("simpletask"); task.delay(1000).wait(); throw new exception("simpletask error"); }); try { task.wait(); } catch (exception ex) { console.writeline(ex.message); } if (task.iscompletedsuccessfully) { console.writeline("iscompleted"); } }
上面的代码模拟了 task 内部发生的异常,并捕获了异常,通常情况下,推荐使用 task 的任务状态判断以进行下一步的任务处理(如果需要),如果仅仅是简单的执行一个异步任务,直接捕获异常即可,这里使用了状态判断,如果任务已完成,则打印一则消息:iscompleted;很明显,在上面的代码中,此 “iscompleted” 消息并不会被打印到控制台
注意,这里使用了 task.iscompletedsuccessfully 而不是 task.iscompleted,这两者的区别在于,前者只有在任务正常执行完成,无异常,无中途退出指令的情况下才会表示已完成,而 task.iscompleted 则仅仅表示“任务完成”
3.2 执行程序,输出结果
4. 同步上下文
在 winform/wpf 应用程序中,也常常需要在 ui 上开辟异步任务,通常情况下,窗体控件仅允许创建其的线程访问,在没有 task 的时代,处理异步上下文到同步上下文是一件非常复杂的事情,在 task 出现以后,提供了 taskscheduler 任务调度器,让我们可以非常方便的在异步线程中访问 ui 线程的资源
4.1 获取当前线程上下文对象
static void tasksynchronizationcontext() { var uisynccontext = taskscheduler.fromcurrentsynchronizationcontext(); var t1 = task.factory.startnew<int>(() => { return 1; }); t1.continuewith((atnt) => { // 从这里访问 ui 线程的资源 console.writeline("从这里访问 ui 线程的资源"); }, uisynccontext); }
从上面的代码可以发现,仅仅需要调用 taskscheduler.fromcurrentsynchronizationcontext() 获得当前线程的同步上下文,然后在执行异步任务的时候传入,即可访问当前线程创建的 ui 资源
5. task 的运行方式
5.1 基于 threadpool 线程池的方式
一个异步任务总是处于队列中,任务队列基于先进先出的原则,最新进入队列的任务总是最先被执行;但是,在多线程的环境下,最先执行并不意味着最先结束,意识到这一点很重要,每个任务可调度的资源和处理的进度决定了任务的完成时间。
默认情况下,所有的任务都使用 threadpool 的资源,当你开启一个 task 的时候,实际上,是由 threadpool 分配了一个线程,threadpool 的上限取决于很多方面的因素,例如虚拟内存的大小,当 task 开启的数量超过threadpool 的上限的时候,task 将进入排队状态,可以手动设置 threadpool 的大小
static void setthreadpool() { var available = threadpool.setmaxthreads(8, 16); console.writeline("result:{0}", available); }
上面的代码表示设置当前程序可使用的线程池大小,但是,setmaxthreads 的值不应该小于托管服务器的 cpu 核心数量,否则,变量 available 的值将显示为 false,表示未成功设置线程池上限
注意:threadpool 上的所有线程都是后台线程,也就是说,其isbackground属性是true,在托管程序退出后,threadpool 也将会退出。
5.2 长时间运行于后台的任务
在创建 task 的时候,我们可能需要做一些长时间运行的业务,这个时候如果使用默认的 threadpool 资源,在并发状态下,这是不合适的,因为该任务总是长时间的占用线程池中的资源,导致线程池数量受限,这种情况下,可以在创建任务的时候使用指定 taskcreationoptions.longrunning 方式创建 task
static void longtask() { task.factory.startnew(() => { console.writeline("longrunning task"); }, taskcreationoptions.longrunning); }
上面的代码看起来和创建普通的 task 任务并没有多大的区别,唯一不同的是,在参数中传入了 taskcreationoptions.longrunning,指定这个是一个 longrunning 类型的任务,当taskfactory 收到这样一个类型的任务时,将会为这个任务开辟一个独立的线程,而不是从 threadpool 中创建
6. 有条件的 task
task 内部提供多种多样的基于队列的链式任务管理方法,通过使用这些快捷方式,可以让异步队列有序的执行,比如continuewith(),continuewhenall(),continuewhenany(),waitall(),waitany(),whenall(),whenany()
6.1 使用演示
static void withtask() { var order1 = task.run(() => { console.writeline("order 1"); }); // 匿名委托将等待 order1 执行完成后执行,并将 order1 对象作为参数传入 order1.continuewith((task) => { console.writeline("order 1 is completed"); }); var t1 = task.run(() => { task.delay(1500).wait(); console.writeline("t1"); }); var t2 = task.run(() => { task.delay(2000).wait(); console.writeline("t2"); }); var t3 = task.run(() => { task.delay(3000).wait(); console.writeline("t3"); }); task.waitall(t1, t2, t3); // t1,t2,t3 完成后输出下面的消息 console.writeline("t1,t2,t3 is complete"); var t4 = task.run(() => { task.delay(1500).wait(); console.writeline("t4"); }); var t5 = task.run(() => { task.delay(2000).wait(); console.writeline("t5"); }); var t6 = task.run(() => { task.delay(3000).wait(); console.writeline("t6"); }); task.waitany(t4, t5, t6); // 当任意任务完成时,输出下面的消息,目前按延迟时间计算,在 t4 完成后立即输出下面的信息 console.writeline("t4,t5,t6 is complete"); var t7 = task.run(() => { task.delay(1500).wait(); console.writeline("t7"); }); var t8 = task.run(() => { task.delay(2000).wait(); console.writeline("t8"); }); var t9 = task.run(() => { task.delay(3000).wait(); console.writeline("t9"); }); var whenall = task.whenall(t7, t8, t9); // whenall 不会等待,所以这里必须显示指定等待 whenall.wait(); // 当所有任务完成时,输出下面的消息 console.writeline("t7,t8,t9 is complete"); var t10 = task.run(() => { task.delay(1500).wait(); console.writeline("t10"); }); var t11 = task.run(() => { task.delay(2000).wait(); console.writeline("t11"); }); var t12 = task.run(() => { task.delay(3000).wait(); console.writeline("t12"); }); var whenany = task.whenall(t10, t11, t12); // whenany 不会等待,所以这里必须显示指定等待 whenany.wait(); // 当任意任务完成时,输出下面的消息,目前按延迟时间计算,在 t10 完成后立即输出下面的信息 console.writeline("t10,t11,t12 is complete"); }
6.2 执行上面的代码,输出结果如下
值得注意的是,当调用 whenall 方法时,会返回执行任务的状态,此状态是所有任务的统一状态,如果执行了 3 个任务,而其中一个出错,则返回任务状态表示为:faulted,如果任意任务被取消,则状态为:canceled;
当调用 whenany() 方法时,表示任意任务完成即可表示完成,此时,会返回最先完成的任务信息
注意:whenall 和 whenany 方法正常执行,无异常,无取消,则所返回的完成状态表示为:rantocompletion
结束语
- 本章简要介绍了基于队列的异步任务(tap)使用方式
- 介绍了tap 运行的方式、以及异常处理
- 同时还介绍了如何使用 ui 线程同步上下文对象,以及有条件使用 tap 的各种方法
示例代码下载
上一篇: 刘备逃跑的时候,为何要带上那么多平民呢?
下一篇: Python给你的头像加上圣诞帽