目录
Task与Thread
Task的用法
1、创建任务
2、async/await的实现方式
3、task可以同步执行吗?
4、Task的Wait、WaitAny、WaitAll方法介绍
5、释放、取消Task
6、Task的Wait、WaitAny、WaitAll方法介绍
ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:
◆ ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
◆ ThreadPool不支持线程执行的先后次序;
以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,FCL中提供了一个功能更强大的概念:Task。Task在线程池的基础上进行了优化,并提供了更多的API。在FCL4.0中,如果我们要编写多线程程序,Task显然已经优于传统的方式。
net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。
无返回值的方式
方式1 : var t1 = new Task(() => TaskMethod("Task 1"));
t1.Start();
Task.WaitAll(t1);//等待所有任务结束
注 : 任务的状态:
Start之前为:Created
Start之后为:WaitingToRun
方式2 : Task.Run(() => TaskMethod("Task 2"));
方式3 : Task.Factory.StartNew(() => TaskMethod("Task 3")); 直接异步的方法
或者 :
var t3=Task.Factory.StartNew(() => TaskMethod("Task 3"));
Task.WaitAll(t3);//等待所有任务结束
注 :
任务的状态:
Start之前为:Running
Start之后为:Running
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var t1 = new Task(() => TaskMethod("Task 1"));
var t2 = new Task(() => TaskMethod("Task 2"));
t2.Start();
t1.Start();
Task.WaitAll(t1, t2);
Task.Run(() => TaskMethod("Task 3"));
Task.Factory.StartNew(() => TaskMethod("Task 4"));
//标记为长时间运行任务,则任务不会使用线程池,而在单独的线程中运行。
Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);
#region 常规的使用方式
Console.WriteLine("主线程执行业务处理.");
//创建任务
Task task = new Task(() =>
{
Console.WriteLine("使用System.Threading.Tasks.Task执行异步操作.");
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
});
//启动任务,并安排到当前任务队列线程中执行任务(System.Threading.Tasks.TaskScheduler)
task.Start();
Console.WriteLine("主线程执行其他处理");
task.Wait();
#endregion
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.ReadLine();
}
static void TaskMethod(string name)
{
Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
}
带返回值的方式
方式4 :
Task
task.Start();
int result = task.Result;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static Task CreateTask(string name)
{
return new Task(() => TaskMethod(name));
}
static void Main(string[] args)
{
TaskMethod("Main Thread Task");
Task task = CreateTask("Task 1");
task.Start();
int result = task.Result;
Console.WriteLine("Task 1 Result is: {0}", result);
task = CreateTask("Task 2");
//该任务会运行在主线程中
task.RunSynchronously();
result = task.Result;
Console.WriteLine("Task 2 Result is: {0}", result);
task = CreateTask("Task 3");
Console.WriteLine(task.Status);
task.Start();
while (!task.IsCompleted)
{
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
Console.WriteLine(task.Status);
result = task.Result;
Console.WriteLine("Task 3 Result is: {0}", result);
#region 常规使用方式
//创建任务
Task getsumtask = new Task(() => Getsum());
//启动任务,并安排到当前任务队列线程中执行任务(System.Threading.Tasks.TaskScheduler)
getsumtask.Start();
Console.WriteLine("主线程执行其他处理");
//等待任务的完成执行过程。
getsumtask.Wait();
//获得任务的执行结果
Console.WriteLine("任务执行结果:{0}", getsumtask.Result.ToString());
#endregion
}
static int TaskMethod(string name)
{
Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(2));
return 42;
}
static int Getsum()
{
int sum = 0;
Console.WriteLine("使用Task执行异步操作.");
for (int i = 0; i < 100; i++)
{
sum += i;
}
return sum;
}
}
}
(异步方法:在执行完成前立即返回调用方法,在调用方法继续执行的过程中完成任务。)
①从 Main 方法执行到 CountCharactersAsync(1, url1) 方法时,该方法会立即返回,然后才会调用它内部的方法开始下载内容。该方法返回的是一个 Task
②这样就可以不必等 CountCharactersAsync(1, url1) 方法执行完成就可以继续进行下一步操作。到执行 CountCharactersAsync(2, url2) 方法时,跟 ① 一样返回 Task
③然后,Main 方法继续执行三次 ExtraOperation 方法,同时两次 CountCharactersAsync 方法依然在持续工作 。
④t1.Result 和 t2.Result 是指从 CountCharactersAsync 方法调用的 Task
async/await 结构可分成三部分:
语法分析:
通过上面的实际代码测试,我们知道task都是异步执行,那么有人会问,task可以实现同步执行吗?不急,强大的微软也想到了这个问题,于是乎,提供了
1)task.RunSynchronously()
来同步执行,但是这种方式执行,只有通过new 实例化的task才有效,原因也很简单,其他两种方式创建task都已经自启动执行了,不可能在来一个同步启动执行吧,嘿嘿。下面我们用代码来演示:
///
/// 通过RunSynchronously 实现task的同步执行
///
private static void TaskRunSynchronously()
{
Console.WriteLine("主线程开始执行!");
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine("Task执行结束!");
return "";
});
/// task.Start();
/// task.Wait();
// 获取执行结果,会阻塞主流程
// string result = task.Result;
//同步执行,task会阻塞主线程
task.RunSynchronously();
Console.WriteLine("执行主线程结束!");
Console.ReadKey();
}
执行结果:很明显主线程阻塞等待task同步执行。
2)task同步执行,出了上面的实现方式,其实我们也可以通过task.wait()来变相的实现同步执行效果,当然也可以用task.Result来变现的实现,原理很简单,因为wait()和Result都是要阻塞主流程,直到task执行完毕,是不是有异曲同工之妙呢!以代码为例:
通过task.wait()实现,只需要对上面的代码做一个简单的调整,如下:其最终的效果一样:
///
/// 通过RunSynchronously 实现task的同步执行
///
private static void TaskRunSynchronously()
{
Console.WriteLine("主线程开始执行!");
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine("Task执行结束!");
return "";
});
task.Start();
task.Wait();
// 获取执行结果,会阻塞主流程
// string result = task.Result;
// 同步执行,task会阻塞主线程
// task.RunSynchronously();
Console.WriteLine("执行主线程结束!");
Console.ReadKey();
}
3)通过task.Result 实现,前提是task一定要有返回值,如下:其最终的效果一样:
///
/// 通过RunSynchronously 实现task的同步执行
///
private static void TaskRunSynchronously()
{
Console.WriteLine("主线程开始执行!");
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine("Task执行结束!");
return "";
});
task.Start();
/// task.Wait();
// 获取执行结果,会阻塞主流程
string result = task.Result;
// 同步执行,task会阻塞主线程
// task.RunSynchronously();
Console.WriteLine("执行主线程结束!");
Console.ReadKey();
}
执行效果也和上面的两种方式一样。
当然我还可以通过task.IsCompleted来变现实现,在此就不在细说,简单一个代码示意即可:while
(!task.IsCompleted){}
当然我上面说的几种实现同步的方式,只是为了拓展一下思路,不一定都是最优方案。
task的基本创建和用法,上面都做了简单的介绍,但是在我们实际业务场景中,往往不是那么简单的单纯实现。比如:还是刚刚上面的那个酒店信息获取为例,现在新的需求是:3个渠道的接口实时数据,我们只需要获取到其中的一个就立即返回会用户,避免用户等待太久,那么这个时候task.WaitAny就派上用场了,WaitAny就是等待一组tsak集合中,只要有一个执行完毕就不在等待,与之对应的是WaitAll需要等待一组tsak集合中所有tsak都执行完毕,当然了Wait是针对一个task的,等待本身执行完成,上面的模拟同步执行已经说了,就不在啰嗦。
///
/// 获取最新的客房信息(只需要获取到一个即可)
///
/// 客房信息集合
private static List GetOneHotelRoomInfro()
{
// 模拟存储获取到的酒店客房数据集合
List listHotelRoomInfro = new List();
Console.WriteLine("下面通过3个task,并行的到不同接口方获取实时的客房信息
:");
Console.WriteLine("");
// 在此我也分别对3种不同渠道,采用3种不同的方式来实现
// 其一、通过传统的 new 方式来实例化一个task对象,获取 携程 的客房数据
Task newCtripTask = new Task(() =>
{
// 具体获取业务逻辑处理...
Thread.Sleep(new Random().Next(100, 1000));
listHotelRoomInfro.Add("我是来自 携程 的最新客房信息");
});
// 启动 tsak
newCtripTask.Start();
// 其二、通过工厂 factory 来生成一个task对象,并自启动:获取 艺龙
的客房数据
Task factoryElongTask = Task.Factory.StartNew(() =>
{
// 具体获取业务逻辑处理...
Thread.Sleep(new Random().Next(100, 1000));
listHotelRoomInfro.Add("我是来自 艺龙 的最新客房信息");
});
// 其三、通过 Task.Run(Action action) 来创建一个自启动task:
获取 去哪儿网 的客房数据
Task runQunarTask = Task.Run(() =>
{
// 具体获取业务逻辑处理...
Thread.Sleep(new Random().Next(100, 1000));
listHotelRoomInfro.Add("我是来自 去哪儿网 的最新客房信息");
});
// 只需要等待一个有返回即可
Task.WaitAny(new Task[] { newCtripTask, factoryElongTask,
runQunarTask });
// 等待所有接口数据返回
// Task.WaitAll(new Task[] { newCtripTask, factoryElongTask,
runQunarTask });
Console.WriteLine("已经有接口数据返回!");
foreach (var item in listHotelRoomInfro)
{
Console.WriteLine($"返回的接口数据为:{item}");
}
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
Console.WriteLine("");
return listHotelRoomInfro;
}
1)Task实现了IDispose接口,而且提供了Dispose方法。这意味着我应该Dispose所有的Task吗?
这是我简短的回答:不,不用非要Dispose你的Task。
这是我中等长度的答案:不用。不用费心去Dispose你的Task,除非性能或者弹性测试需要你去基于使用方式去Dispose Task来达到性能目标。当你需要去Dispose 那些Task,仅在情形简单的时候去做,即当你100%确认代码中Task已经成功完成(IsCompleted为true)而且没有其他人使用这些Task。
如果有喝咖啡并阅读的时间,可以看看下面的长答案:在高的设计层面,.Net Framework设计准备表明如果一个类型持有其他的IDispose资源,那么它应该实现IDispose接口。所以Task有Dispose方法。在内部,Task可以分配一个被用来等待Task成功完成的WaitHandle。WaitHandle实现了IDispose接口,因为它内部持有实现了IDispose的SafeWaitHandle。SafeWaitHandle包含一个本地Handle资源:如果SafeWaitHandle没有Dispose,最终它的终结器(finalizer)会清理所有的被包含的handle资源,但是与此同时它的资源不会被清理干净,这会给系统造成压力。通过Task实现IDispose接口,我们使得关心积极清理这些资源的开发者可以及时地清理这些资源。
带来的问题
如果每一个Task分配一个WaitHandle,出于性能考虑,积极地dispose这些task是个好主意。但事实并非如此。事实上很少的task真正地分配了WaitHandle。在.Net 4里面,WaitHandle在几种情况下被延迟加载:
如果Task.IAsyncResult.AsyncWaitHandle属性(显式实现了接口)被访问;
如果Task被Task.WaitAll或者Task.WaitAny调用,且Task尚未成功完成。
而且在.Net 4中,一旦Task被dispose了,它的大多数成员在被访问的时候会抛出ObjectDisposedExceptions异常。这使得缓存已经完成的task变得困难(可能因为性能原因缓存task),因为一旦一个消费者dispose了某个task,另一个消费者将不能访问这个task的重要成员,比如ContinueWith或者这个task的Result。
2)CancellationTokenSource用于取消基于Task建立的线程(单个线程、多个线程都可以)
如果 CacellationToken 在 Task 调度前取消, Task 会被取消,永远都不会执行。但是,如果Task 已调度,那么Task 为了允许它的操作在执行期间取消,Task 的代码就必须显示支持取消。也就是在一个已经开始执行的任务时,单单使用Cancel 函数是不起作用的,任务根本不会被取消。
CancellationTokenSource cts = new CancellationTokenSource ();
Task.Run(() =>{……} , cts.Token);//将cts.Token传入任务中,在外部通过控制cts实现对任务的控制
cts.Cancel();//传达取消请求
bool taskState = cts.IsCancellationRequested;//判断任务是否取消,放while中用于跳出循环
CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000)
实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action)
注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。
static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
//注册任务取消的事件
source.Token.Register(() =>
{
Console.WriteLine("任务被取消后执行xx操作!");
});
int index = 0;
//开启一个task执行任务
Task task1 = new Task(() =>
{
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
source.CancelAfter(5000);
Console.ReadKey();
}
执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:
Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?
这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。 task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。
static void Main(string[] args)
{
Task task1 = new Task(() => {
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 = new Task(() => {
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//task1,task2执行完了后执行后续操作
Task.WhenAll(task1, task2).ContinueWith((t) => {
Thread.Sleep(100);
Console.WriteLine("执行后续操作完毕!");
});
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把栗子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作,这里不再演示。