任务,基于线程池,并在线程池的基础上进行了优化,提供了更多的API,这些 API 支持等待、取消、继续、可靠的异常处理、详细状态、自定义计划等功能。其使我们对并行编程变得更简单,且不用关心底层是怎么实现的。并行编程就像现实中,我们开发项目,就是一个并行的例子,把不同的模块分给不同的人,同时进行,才能在短的时间内做出大的项目。
创建Task的方法有多种,下面来看一下官网例子:三个任务执行一个名为 action的 Action
1、任务 t1 通过调用任务类构造函数进行实例化,但只有在启动任务 t2 之后,才通过调用其 Start() 方法来启动。
2、通过调用TaskFactory.StartNew(Action 方法在单个方法调用中实例化和启动任务 t2。
3、通过调用Run(Action) 方法在单个方法调用中实例化和启动任务 t3。Task.Run跟Task.Factory.StarNew和new Task相差不多,不同的是前两种是放进线程池立即执行,而Task.Run则是等线程池空闲后在执行。
4、通过调用RunSynchronously() 方法,在主线程上同步执行任务 t4。
由于 task t4 同步执行,因此它在主应用程序线程上执行。 其余任务通常在一个或多个线程池线程上异步执行。
Action<object> action = (object obj) =>
{
Console.WriteLine("Task={0}, obj={1}, Thread={2}",Task.CurrentId, obj,Thread.CurrentThread.ManagedThreadId);
};
// Create a task but do not start it.
Task t1 = new Task(action, "task");
// Construct a started task
Task t2 = Task.Factory.StartNew(action, "Factory");
// Block the main thread to demonstrate that t2 is executing
t2.Wait();
// Launch t1
t1.Start();
Console.WriteLine("t1 has been launched. (Main Thread={0})",Thread.CurrentThread.ManagedThreadId);
// Wait for the task to finish.
t1.Wait();
// Construct a started task using Task.Run.
string taskData = "run";
Task t3 = Task.Run(() => {Console.WriteLine("Task={0}, obj={1}, Thread={2}",Task.CurrentId, taskData,Thread.CurrentThread.ManagedThreadId);});
// Wait for the task to finish.
t3.Wait();
// Construct an unstarted task
Task t4 = new Task(action, "RunSynchronously");
// Run it synchronously
t4.RunSynchronously();
// Although the task was run synchronously, it is a good practice to wait for it in the event exceptions were thrown by the task.
t4.Wait();
输出结果如下:
构造函数:比较常用的是前两种
下面通过一个简单例子来看一下Task的声明周期,编写如下代码:
var task1 = new Task(() =>
{
Console.WriteLine("Begin");
System.Threading.Thread.Sleep(2000);
Console.WriteLine("Finish");
});
Console.WriteLine("Before start:" + task1.Status+ "\tIsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task1.IsCanceled, task1.IsCompleted, task1.IsFaulted);
task1.Start();
Console.WriteLine("After start:" + task1.Status + "\tIsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task1.IsCanceled, task1.IsCompleted, task1.IsFaulted);
task1.Wait();
Console.WriteLine("After Finish:" + task1.Status + "\tIsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task1.IsCanceled, task1.IsCompleted, task1.IsFaulted);
Console.Read();
其输出结果如下:
可以看到调用Start前的状态是Created,然后等待分配线程去执行,到最后执行完成。
从我们可以得出Task的简略生命周期:
Created:表示默认初始化任务,但是“工厂创建的”实例直接跳过。
WaitingToRun: 这种状态表示等待任务调度器分配线程给任务执行。
RanToCompletion:任务执行完毕。
由于 Task 对象执行的工作通常在线程池线程上异步执行,而不是在主应用程序线程上同步执行,因此您可以使用 Status 属性,还可以使用 IsCanceled、IsCompleted和 IsFaulted 属性,用于确定任务的状态。
Task最吸引人的地方就是他的任务控制了,你可以很好的控制task的执行顺序,让多个task有序的工作。下面来详细说一下:
若要等待单个任务完成,可以调用其 Task.Wait 方法。 调用 Wait 方法会阻止调用线程,直到单类实例执行完毕。无参数Wait() 方法无条件等待,直到任务完成。 Wait(Int32) 和Wait(TimeSpan) 方法会阻止调用线程,直到任务完成或超时间隔结束(以先达到者为准)。通过调用 Wait(CancellationToken) 和Wait(Int32, CancellationToken) 方法来提供取消标记。 如果在执行 Wait 方法时令牌的 IsCancellationRequested 属性 true 或变为 true,则该方法将引发 OperationCanceledException。
// Wait on a single task with a timeout specified.
Task taskA = Task.Run(() => Thread.Sleep(2000));
try
{
taskA.Wait(1000); // Wait for 1 second.
bool completed = taskA.IsCompleted;
Console.WriteLine("Task A completed: {0}, Status: {1}",completed, taskA.Status);
if (!completed)
Console.WriteLine("Timed out before task A completed.");
}
catch (AggregateException)
{
Console.WriteLine("Exception in taskA.");
}
看字面意思就知道,就是等待所有的任务都执行完成,下面我们来写一段代码演示一下:
var tasks = new Task[3];
var rnd = new Random();
for (int ctr = 0; ctr <= 2; ctr++)
tasks[ctr] = Task.Run(() => Thread.Sleep(rnd.Next(500, 3000)));
try
{
Task.WaitAll(tasks);
Console.WriteLine("Status of all tasks:");
foreach (var t in tasks)
Console.WriteLine(" Task #{0}: {1}", t.Id, t.Status);
}
catch (AggregateException)
{
Console.WriteLine("An exception occurred.");
}
这个方法会返回一个索引值,指明完成的是哪一个Task对象。用法同Task.WaitAll,就是等待任何一个任务完成就继续向下执行,将上面的代码WaitAll替换为WaitAny,输出结果如下:
在实际应用中,出现异常或者用户点击取消等等,我们就需要取消这个任务。那么如何取消一个Task呢?我们通过Cancellation的tokens来取消一个Task。在很多Task的Body里面包含循环,我们可以在轮询的时候判断IsCancellationRequested属性是否为True,如果是True的话可以使用以下选项之一终止操作:
1)简单地从委托中返回。 在许多情况下,这样已足够;但是,采用这种方式取消的任务实例会转换为 TaskStatus.RanToCompletion 状态,而不是 TaskStatus.Canceled状态。
2)引发 OperationCanceledException ,并将其传递到在其上请求了取消的标记。 完成此操作的首选方式是使用 ThrowIfCancellationRequested方法。 采用这种方式取消的任务会转换为 Canceled 状态,调用代码可使用该状态来验证任务是否响应了其取消请求。
如果你在等待转换为 Canceled 状态的任务,则会引发 System.Threading.Tasks.TaskCanceledException 异常(包装在AggregateException 异常中)。 请注意,此异常指示成功的取消,而不是有错误的情况。
下面来看一个例子:这里开启了一个Task,并给token注册了一个方法,输出一条信息,然后执行ReadKey开始等待用户输入,用户点击回车后,执行tokenSource.Cancel方法,取消任务。
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() =>
{
for (var i = 0; i < 1000; i++)
{
System.Threading.Thread.Sleep(1000);
if (token.IsCancellationRequested)
{
//在取消标志引用的CancellationTokenSource上如果调用Cancel,就会抛出OperationCanceledException
token.ThrowIfCancellationRequested();
Console.WriteLine("Abort mission success!");
return;
}
}
}, token);
//#region 注册回调函数,当CancellationTokenSource.Cancel()执行后,调用回调函数
token.Register(() =>{ Console.WriteLine("此处回调函数");});
Console.WriteLine("Press enter to cancel task...");
Console.ReadKey();
tokenSource.Cancel();
try
{
task.Wait();
}
catch (AggregateException ex)
{
foreach (var e in ex.Flatten().InnerExceptions)
Console.WriteLine(" {0}: {1}", e.GetType().Name, e.Message);
//将任何OperationCanceledException对象都视为已处理。其他任何异常都造成抛出一个AggregateException,其中只包含未处理的异常
ex.Handle(e => e is OperationCanceledException);
}
finally
{
Console.WriteLine("Status:{0}, Has Exceprion:{1} ", task.Status, task.Exception != null);
}
接下来注释第12行代码token.ThrowIfCancellationRequested();此行代码运行结果如下:
可以看出,任务的状态不是Canceled而是RanToCompletion。
官网例子:下面的示例创建了10个任务,这些任务将循环,直到计数器的值递增为2000000。当前5个任务达到2000000时,取消标记将被取消,并且任何其计数器未达到2000000的任务都将被取消。然后,该示例检查每个任务的 Status 属性,以指示该任务是否已成功完成或已取消。 对于已完成的,它会显示任务返回的值。
var tasks = new List<Task<int>>();
var source = new CancellationTokenSource();
var token = source.Token;
int completedIterations = 0;
for (int n = 0; n <= 9; n++)
tasks.Add(Task.Run(() =>
{
int iterations = 0;
for (int ctr = 1; ctr <= 2000000; ctr++)
{
token.ThrowIfCancellationRequested();
iterations++;
}
Interlocked.Increment(ref completedIterations);
if (completedIterations >= 5)
source.Cancel();
return iterations;
}, token));
Console.WriteLine("Waiting for the first 10 tasks to complete...\n");
try
{
Task.WaitAll(tasks.ToArray());
}
catch (AggregateException)
{
Console.WriteLine("Status of tasks:\n");
Console.WriteLine("{0,10} {1,20} {2,14:N0}", "Task Id", "Status", "Iterations");
foreach (var t in tasks)
Console.WriteLine("{0,10} {1,20} {2,14}", t.Id, t.Status, t.Status != TaskStatus.Canceled ? t.Result.ToString("N0") : "n/a");
}
运行结果如下:
有关详细信息和示例,请参阅使用延续任务链接任务和如何:取消任务及其子级。
创建一个在目标 Task 完成时异步执行的延续任务。在实际应用程序中,延续委托可能会记录有关异常的详细信息,并可能生成新任务以从异常中恢复。
要写可伸缩的软件,一定不能使你的线程阻塞。这意味着如果调用Wait或者在任务未完成时查询Result属性,极有可能造成线程池创建一个新线程,这增大了资源的消耗,并损害了伸缩性。ContinueWith便是一个更好的方式,一个任务完成时它可以启动另一个任务,实现Task的延续。使用 TaskContinuationOptions枚举中的值,用于设置计划延续任务的时间以及延续任务的工作方式的选项。 这包括条件(如 OnlyOnCanceled)和执行选项(如 ExecuteSynchronously)。
下面看代码示例:
//使用了Task(Func
var t = Task.Factory.StartNew(i => { return (int)i + 100; }, 10000);
t.ContinueWith(task => Console.WriteLine("The result is:{0}", task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
t.ContinueWith(task => Console.WriteLine("{0}: {1}", t.Exception.InnerException.GetType().Name, t.Exception.InnerException.Message), TaskContinuationOptions.OnlyOnFaulted);
t.ContinueWith(task => Console.WriteLine("cancel:" + task.IsCanceled), TaskContinuationOptions.OnlyOnCanceled);
在一个Task运行完成后,可用Result属性获取结果值。访问属性的 get 访问器会阻止调用线程, 直到异步操作完成;它等效于调用Wait方法。
操作结果可用后, 它将被存储并在对属性的Result后续调用后立即返回。请注意, 如果在任务的操作过程中发生异常, 或者如果任务已取消, 则Result属性不会返回值。 相反, 尝试访问属性值会引发AggregateException异常。
Task.Run(() => { return "One"; }).ContinueWith(ss => { Console.WriteLine(ss.Result); });
结果输出:One
Task中的嵌套分为两种,关联嵌套和非关联嵌套,就是说内层的Task(子任务)和外层的Task(父任务)是否有联系。默认情况下,子任务独立于父任务。并且必须显式指定 TaskCreationOptions.AttachedToParent 选项来创建附加子任务。
分离子任务:下面实例中子任务与父任务随机先完成。
var parent = Task.Factory.StartNew(() => {
Console.WriteLine("Outer task executing.");
var child = Task.Factory.StartNew(() => {
Console.WriteLine("Nested task starting.");
Thread.SpinWait(500000);
Console.WriteLine("Nested task completing.");
});
});
parent.Wait();
Console.WriteLine("Outer has completed.");
运行结果:
附加子任务:不同于分离子任务,附加子任务与父任务紧密同步,当所有附加子任务完成后父任务才会显示完成。可以通过使用任务创建语句中的 TaskCreationOptions.AttachedToParent 选项,将之前示例中的分离子任务更改为附加子任务,如以下示例中所示。
var parent = Task.Factory.StartNew(() => {
Console.WriteLine("Parent task executing.");
var child = Task.Factory.StartNew(() => {
Console.WriteLine("Attached child starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completing.");
}, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
Console.WriteLine("Parent has completed.");
注意:如果父任务是通过调用 Task.Run 方法而创建的,则可以隐式阻止子任务附加到其中。使用TaskCreationOptions.LongRunning标记为长时间运行的任务,则任务不会使用线程池,而在单独的线程中运行。
由在任务内部运行的用户代码引发的未处理异常会传播回调用线程。如果使用静态或实例 Task.Wait 方法之一,异常会传播,异常处理方法为将调用封闭到 try/catch 语句中。 如果任务是所附加子任务的父级,或在等待多个任务,那么可能会引发多个异常。
为了将所有异常传播回调用线程,任务基础结构会将这些异常包装在 AggregateException 实例中。 AggregateException 异常具有 InnerExceptions 属性,可枚举该属性来检查引发的所有原始异常,并单独处理(或不处理)每个异常。 也可以使用 AggregateException.Handle 方法处理原始异常,筛选掉可视为“已处理”的异常,而无需进一步使用任何逻辑。
例子:
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
tokenSource.Cancel();
Task[] tasks = new Task[2];
tasks[0] = Task.Run(() => { throw new NotImplementedException("Task Error!"); });
tasks[1] = Task.Run(() => { token.ThrowIfCancellationRequested(); }, token);
try
{
Task.WaitAll(tasks);
}
catch (AggregateException ae)
{
//将异常视为“已处理”,而无需进一步使用任何逻辑。
//ae.Handle(ex => { return true; });
Console.WriteLine("One or more exceptions occurred:");
foreach (var e in ae.InnerExceptions)
Console.WriteLine(" {0}: {1}", e.GetType().Name, e.Message);
Console.WriteLine("\nStatus of tasks:");
foreach (var t in tasks)
{
Console.WriteLine(" Task #{0}: {1}", t.Id, t.Status);
//通过使用 Task.Exception 属性观察异常。推荐使用仅在前面的任务出错时才运行的延续任务观察 Exception 属性。
if (t.Exception != null)
{
foreach (var ex in t.Exception.InnerExceptions)
Console.WriteLine(" {0}: {1}", ex.GetType().Name, ex.Message);
}
}
}
运行结果:
附加任务异常处理:如果某个任务具有引发异常的附加子任务,则会在将该异常传播到父任务之前将其包装在 AggregateException 中,父任务将该异常包装在自己的 AggregateException 中,然后再将其传播回调用线程。 在这种情况下,在 Task.Wait、WaitAny、或 WaitAll 方法处捕获的 AggregateException 异常的 InnerExceptions 属性包含一个或多个 AggregateException 实例,而不包含导致错误的原始异常。
var task1 = Task.Factory.StartNew(() =>
{
var child1 = Task.Factory.StartNew(() =>
{
var child2 = Task.Factory.StartNew(() =>
{
// This exception is nested inside three AggregateExceptions.
throw new NotImplementedException("Attached child2 faulted.");
}, TaskCreationOptions.AttachedToParent);
// This exception is nested inside two AggregateExceptions.
throw new ArgumentException("Attached child1 faulted.");
}, TaskCreationOptions.AttachedToParent);
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.Flatten().InnerExceptions)
Console.WriteLine(" {0}: {1}", e.GetType().Name, e.Message);
}
运行结果:
分离任务异常处理:默认情况下,子任务在创建时处于分离状态。必须在直接父任务中处理或重新引发从分离任务引发的异常;将不会采用与附加子任务传播回异常相同的方式将这些异常传播回调用线程。 最顶层的父级可以手动重新引发分离子级中的异常,以使其包装在 AggregateException 中并传播回调用线程。
var task1 = Task.Run(() =>
{
var nested1 = Task.Run(() => { throw new ArgumentException("Detached child task faulted."); });
// Here the exception will be escalated back to the calling thread.We could use try/catch here to prevent that.
nested1.Wait();
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.Flatten().InnerExceptions)
Console.WriteLine(" {0}: {1}", e.GetType().Name, e.Message);
}
一个工厂对象,可创建多种 Task 和 Task
下面的示例使用静态Factory属性以使两个调用TaskFactory
Random rnd = new Random();
Task<int>[] tasks = new Task<int>[2];
tasks[0] = Task.Factory.StartNew(() => rnd.Next(1, 100));
tasks[1] = Task.Factory.StartNew(() => rnd.Next(1, 100));
Task.Factory.ContinueWhenAll(tasks, completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), CancellationToken.None)
.ContinueWith(t => Console.WriteLine("The maxinum is: " + t.Result), TaskContinuationOptions.ExecuteSynchronously).Wait(); // Wait用于测试;
您还可以调用之一TaskFactory
CancellationTokenSource cts = new CancellationTokenSource();
TaskFactory<int> factory = new TaskFactory<int>(cts.Token, TaskCreationOptions.PreferFairness, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
var t1 = factory.StartNew(() => { Console.WriteLine("Task={0},Thread={1}", Task.CurrentId, Thread.CurrentThread.ManagedThreadId); return 0; });
var t2 = factory.StartNew(() => { Console.WriteLine("Task={0},Thread={1}", Task.CurrentId, Thread.CurrentThread.ManagedThreadId); return 0; });
cts.Dispose();
在大多数情况下,您不需要实例化一个新TaskFactory
实例1:体现多任务如何协作。如图:
实现代码:
var task = Task.Run(() =>
{
var t1 = Task.Run(() => { Console.WriteLine("Task 1 running..."); return 1; });
var t2 = Task.Run(() => { Console.WriteLine("Task 2 running..."); return t1.Result + 2; });
var t3 = Task.Run(() => { Console.WriteLine("Task 3 running..."); return t1.Result + 3; });
var t4 = Task.Run(() => { Console.WriteLine("Task 4 running..."); return t2.Result + 4; });
Console.WriteLine("Task Finished! The result is {0}", t3.Result + t4.Result);
});
5天玩转C#并行和多线程编程
C#线程篇—Task(任务)和线程池不得不说的秘密(5)