前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【推荐】C#线程篇---Task(任务)和线程池不得不说的秘密(5.2)

【推荐】C#线程篇---Task(任务)和线程池不得不说的秘密(5.2)

作者头像
逸鹏
发布2018-04-10 16:45:25
1.6K0
发布2018-04-10 16:45:25
举报
文章被收录于专栏:逸鹏说道逸鹏说道

ContinueWith? 啥东西~~??

 要写可伸缩的软件,一定不能使你的线程阻塞。这意味着如果调用Wait或者在任务未完成时查询Result属性,极有可能造成线程池创建一个新线程,这增大了资源的消耗,并损害了伸缩性。

  ContinueWith便是一个更好的方式,一个任务完成时它可以启动另一个任务。上面的例子不会阻塞任何线程。

  当Sum的任务完成时,这个任务会启动另一个任务以显示结果。ContinueWith会返回对新的Task对象的一个引用,所以为了看到结果,我需要调用一下Wait方法,当然你也可以查询下Result,或者继续ContinueWith,返回的这个对象可以忽略,它仅仅是一个变量。

  还要指出的是,Task对象内部包含了ContinueWith任务的一个集合。所以,实际上可以用一个Task对象来多次调用ContinueWith。任务完成时,所有ContinueWith任务都会进入线程池队列中,在构造ContinueWith的时候我们可以看到一个TaskContinuationOptions枚举值,不能忽视,看看它的定义:

PrefereFairness是尽量公平的意思,就是较早调度的任务可能较早的运行,先来后到,将线程放到全局队列,便可以实现这个效果。

ExecuteSynchronously指同步执行,强制两个任务用同一个线程一前一后运行,然后就同步运行了。

看得是不是晕乎乎 ?有这么多枚举例子,怎么掌握啊?多看几次,知道任务的使用情况,以后用起来得心应手~想学新技术,就要能耐住,才能基础牢固。来看个例子,用用这些枚举。

static void Main(string[] args)
        {
            Task<Int32> t = new Task<Int32>(i => Sum((Int32)i),10000);


            t.Start();


            t.ContinueWith(task=>Console.WriteLine("The sum is:{0}",task.Result),
                TaskContinuationOptions.OnlyOnRanToCompletion);
 
            t.ContinueWith(task=>Console.WriteLine("Sum throw:"+task.Exception),
                TaskContinuationOptions.OnlyOnFaulted);
 
            t.ContinueWith(task=>Console.WriteLine("Sum was cancel:"+task.IsCanceled),
                TaskContinuationOptions.OnlyOnCanceled);
            try
            {
                t.Wait();  // 测试用
            }
            catch (AggregateException)
            {
                Console.WriteLine("出错");
            }
 
 
        }


        private static Int32 Sum(Int32 i)
        {
            Int32 sum = 0;
            for (; i > 0; i--)
            {
                checked { sum += i; }
            }
 
            return sum;
        }
    }

 ContinueWith讲完了。可是还没有结束哦。

  AttachedToParnt枚举类型(父任务)也不能放过!看看怎么用,写法有点新奇,看看:

static void Main(string[] args)
        {
            Task<Int32[]> parent = new Task<Int32[]>(() => {
                var results = new Int32[3];
                //
                new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start();
                return results;
            });


            var cwt = parent.ContinueWith( parentTask=>Array.ForEach(parentTask.Result,Console.WriteLine));
 


            parent.Start();
            cwt.Wait();
        }


        private static Int32 Sum(Int32 i)
        {
            Int32 sum = 0;
            for (; i > 0; i--)
            {
                checked { sum += i; }
            }
            return sum;
        }
    }

Oh,我都写晕了。。。(+﹏+)~ 例子中,父任务创建兵启动3个Task对象。默认情况下,一个任务创建的Task对象是顶级任务,这些任务跟创建它们的那个任务没有关系。

TaskCreationOptions.AttachedToParent标志将一个Task和创建它的那个Task关联起来,除非所有子任务(子任务的子任务)结束运行,否则创建任务(父任务)不会认为已经结束。调用ContinueWith方法创建一个Task时,可以指定TaskContinuationOptions.AttachedToParent标志将延续任务置顶为一个子任务。

  看了这么多任务的方法操作示例了,现在来挖挖任务内部构造

  每个Task对象都有一组构成任务状态的字段。

  •   一个Int32 ID(只读属性)
  • 代表Task执行状态的一个Int32
  • 对父任务的一个引用
  • 对Task创建时置顶TaskSchedule的一个引用
  • 对回调方法的一个引用
  • 对要传给回调方法的对象的一个引用(通过Task只读AsyncState属性查询)
  • 对一个ExceptionContext的引用
  • 对一个ManualResetEventSlim对象的引用

还有没个Task对象都有对根据需要创建的一些补充状态的一个引用,补充状态包含这些:

  • 一个CancellationToken
  • 一个ContinueWithTask对象集合
  • 为抛出未处理异常的子任务,所准备的一个Task对象集合

说了这么多,只想要大家知道:

  虽然任务提供了大量功能,但并不是没有代价的。因为必须为所有的这些状态分配内存。

如果不需要任务提供的附加功能,使用ThreadPool.QueueUserWorkItem,资源的使用效率会更高一些。

Task类还实现了IDispose接口,允许你在用完Task对象后调用Dispose,不过大多数不管,让垃圾回收器回收就好。

创建一个Task对象时,代表Task唯一的一个Int32字段初始化为零,TaskID从1开始,每分配一个ID都递增1。顺带说一下,在你调试中查看一个Task对象的时候,会造成调试器显示Task的ID,从而造成为Task分配一个ID。

  这个ID的意义在于,每个Task都可以用一个唯一的值来标识。Visual Studio会在它的“并行任务”和并行堆栈“窗口中显示这些任务ID。要知道的是,这是Visual Studio自己分配的ID,不是在自己代码中分配的ID,几乎不可能将Visual Studio分配的ID和代码正在做的事情联系起来。要查看自己正在运行的任务,可以在调试的时候查看Task的静态CurrentId属性,如果没有任务在执行,CurrentId返回null。

  再看看TaskStatus的值,这个可以查询Task对象的生存期:

这些在任务运行的时候都是可以一一查到的,还有~判断要像这样:

1 if(task.Status==TaskStatus.RantoCompletion)...

为了简化编码,Task只提供几个只读Boolean属性:IsCanceled,IsFaulted,IsCompleted,它们能返回最终状态true/false。 如果Task是通过调用某个函数来创建的,这个Task对象就会出于WaitingForActivation状态,它会自动运行。

最后我们要来了解一下TaskFactory(任务工厂):

 1.需要创建一组Task对象来共享相同的状态

  2.为了避免机械的将相同的参数传给每一个Task的构造器。

满足这些条件就可以创建一个任务工厂来封装通用的状态。TaskFactory类型和TaskFactory<TResult>类型,它们都派生System.Object。

你会学到不一样的编码方式:

static void Main(string[] args)
        {
            Task parent = new Task(() =>
            {
                var cts = new CancellationTokenSource();
                var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);


                //创建并启动3个子任务
                var childTasks = new[] {
            tf.StartNew(() => Sum(cts.Token, 10000)),
            tf.StartNew(() => Sum(cts.Token, 20000)),
            tf.StartNew(() => Sum(cts.Token, Int32.MaxValue))  // 这个会抛异常
         };


                // 任何子任务抛出异常就取消其余子任务
                for (Int32 task = 0; task < childTasks.Length; task++)
                    childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);


                // 所有子任务完成后,从未出错/未取消的任务获取返回的最大值
                // 然后将最大值传给另一个任务来显示最大结果
                tf.ContinueWhenAll(childTasks,
                   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用于测试
            });


            // 子任务完成后,也显示任何未处理的异常
            parent.ContinueWith(p =>
            {
                // 用StringBuilder输出所有


                StringBuilder sb = new StringBuilder("The following exception(s) occurred:" + Environment.NewLine);
                foreach (var e in p.Exception.Flatten().InnerExceptions)
                    sb.AppendLine("   " + e.GetType().ToString());
                Console.WriteLine(sb.ToString());
            }, TaskContinuationOptions.OnlyOnFaulted);


            // 启动父任务
            parent.Start();


            try
            {
                parent.Wait(); //显示结果
            }
            catch (AggregateException)
            {
            }
        }


        private static Int32 Sum(CancellationToken ct, Int32 n)
        {
            Int32 sum = 0;
            for (; n > 0; n--)
            {
                ct.ThrowIfCancellationRequested();
                checked { sum += n; }
            }
            return sum;
        }
    }

任务工厂就这么用,就是一个任务的集合。

现在看看TaskScheduler(任务调度)

  任务基础结构是很灵活的,TaskScheduler对象功不可没。

  TaskScheduler对象负责执行调度的任务,同时向Visual Studio调试器公开任务信息,就像一座桥梁,让我们能够掌控自己的任务线程。

  TaskScheduler有两个派生类:thread pool task scheduler(线程池任务调度),和synchronization context task scheduler(同步上下文任务调度器)。默认情况下,所以应用程序使用的都是线程池任务调度器,这个任务调度器将任务调度给线程池的工作者线程。可以查询TaskScheduler的静态Default属性来获得对默认任务调度器的一个引用。

  同步上下文任务调度器通常用于桌面应用程序,Winfrom,WPF及Silverlight。这个任务调度器将多有任务都调度给应用程序的GUI线程,使所有任务代码都能成功更新UI组建,比如按钮、菜单项等。同步上下文任务调度器根本不使用线程池。同样,可以查询TaskScheduler的静态FromCurrentSynchronizationContext方法来获得对一个同步上下文任务调度器的引用。

就像这样创建类型:

1 //同步上下文任务调度2 TaskScheduler m_syncContextTaskScheduler =3            TaskScheduler.FromCurrentSynchronizationContext();

任务调度有很多的,下面列举一部分,供参考,更多的请参看http://code.msdn.microsoft.com/ParExtSamples 它包括了大量的示例代码。

  它内容实在有点多。写了我很久了。好不容易把任务这块一次写完,希望大家有更多的收获。

 --------------------下篇预告,线程池如何管理线程。把基础介绍完结,也算是一个新的起点了。^_^

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-08-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我为Net狂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档