• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

CLRviaC#读书笔记-27.计算限制的异步操作(上篇)

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

前言

学习这件事情是一个习惯,不能停。。。另外这篇已经看过两个月过去,但觉得有些事情不总结跟没做没啥区别,遂记下此文

1.CLR线程池基础

2.ThreadPool的简单使用练习

3.执行上下文

4.协作式取消和超时,System.Threading.CancellationTokenSource的简单使用

5.任务

6.任务调度器

一、CLR线程池基础

如26章所述,创建和销毁线程是一个昂贵的操作,要耗费大量的时间。另外太多的线程会浪费内存资源。由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还对性能不利。

为了改善这个情况,CLR包含了代码管理它自己的线程池(thread pool),线程池是你的应用程序能使用的线程的集合。

每CLR一个线程池,这个线程池由CLR控制的所有AppDomain共享。

CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序执行一个异步操作时,就调用某个方法,将一个记录项(entry)追加到线程池的队列中,线程池的代码从这个队列中提取记录项,将这个记录项派发(dispatch)给一个线程池线程。如果线程池中没有线程,就创建一个新线程。

如果应用程序向线程池发出许多请求,线程池会尝试只用一个线程来服务所有请求。然而,如果你的应用程序发出请求的速度超过了线程池线程处理它们的速度,就会创建额外的线程。

当一个线程池线程闲着没事一段时间之后,线程会自己醒来终止自己以释放资源。

二、ThreadPool的简单使用练习

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"Main Thread,当前线程:{Thread.CurrentThread.ManagedThreadId}");
            ThreadPool.QueueUserWorkItem(Calculate,5);
            Console.WriteLine($"Main Thread doing other work,当前线程:{Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            Console.WriteLine("hi <Enter> to end this program~~");
            Console.Read();
        }

        //这个方法的签名必须匹配waitcallback委托
        public static void Calculate(object state)
        {
            //这个方法由一个线程池线程执行
            Console.WriteLine($"In Calculate:state={state},当前线程:{Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            //这个方法返回后,线程回到池中,等待另一个任务
        }
    }
View Code

运行结果:

有时上图标注这两行输出结果顺序会颠倒,这是因为两个方法相互之间是异步运行的,windows调度器决定先调度哪一个线程。

三、执行上下文

每个线程都关联一个执行上下文数据结构。

执行上下文(execution context)包括的东西有安全设置(压缩栈、Thread的Principal属性和Windows的身份)、宿主设置(System.Threading.HostExecutionContextManager)以及逻辑调用上下文数据(参见System.Runtime.Remoting.Messaging.CallContext的LogicalSetData和LogicalGetData方法)。

默认情况下,CLR自动造成初始线程的执行上下文“流向”任何辅助线程。这造成将上下文信息传给辅助线程,但这会对性能造成一定影响。

这是因为执行上下文中包含大量信息,而收集所有这些信息,再把它们复制到辅助线程,要耗费不少时间。

System.Threading.ExecutionContext类,允许你控制线程的执行上下文如何从一个线程“流向”另一个。可用这个类 阻止上下文流动以提升应用程序的性能。

    class Program
    {
        static void Main(string[] args)
        {
            //将一些数据放到Main线程的逻辑调用上下文中
            CallContext.LogicalSetData("Name", "Michael");
            //初始化要由线程池线程做的一些工作
            //线程池线程能访问逻辑调用上下文结构
            ThreadPool.QueueUserWorkItem(
                state => Console.WriteLine($"Name={CallContext.LogicalGetData("Name")}"));
            //阻止Main线程的执行上下文的流动
            ExecutionContext.SuppressFlow();
            //初始化要由线程池做的工作
            //线程池线程不能访问逻辑调用上下文数据
            ThreadPool.QueueUserWorkItem(
                state => Console.WriteLine($"Name={CallContext.LogicalGetData("Name")}"));
            //恢复Main线程的执行上下文的流动,
            //以免将来使用更多的线程池线程
            ExecutionContext.RestoreFlow();

            Console.ReadLine();
        }
    }
View Code

编译后运行结果如下:

四、协作式取消和超时,System.Threading.CancellationTokenSource的简单使用

Microsoft.NET Framework提供了标准的取消操作模式。这个模式是协作式的,意味着要取消的操作必须显式支持取消。

CancellationToken实例是轻量级值类型,包含单个私有字段,即对其CancellationTokenSource对象的引用。

在计算限制操作的循环中,可定时调用CancellationToken的IsCancellationRequsted属性,了解循环是否应该提前终止,从而终止计算限制的操作。

提前终止的好处在于,CPU不需要再把时间浪费在你对结果不感兴趣的操作上。

    static void Main(string[] args)
        {
            Go();
        }

        public static void Go()
        {
            CancellationTokenSource token = new CancellationTokenSource();
            //将CancellationTokenSource和参数 传入操作
            ThreadPool.QueueUserWorkItem(
                o => Count(token,1000));
            Console.WriteLine($"Hit <Enter> to cancel operation");
            Console.ReadLine();
            token.Cancel();//如果Count方法已返回,Cancel没有任何效果
            //执行cancel后 立即返回,方法从这里继续运行
            Console.ReadLine();
        }
        public static void Count(CancellationTokenSource token,Int32 counto)
        {
            for (int count = 0; count < counto; count++)
            {
                if(token.IsCancellationRequested)
                {
                    Console.WriteLine("操作被取消");
                    break;
                }
                Console.WriteLine(count);
                Thread.Sleep(200); //出于显示目的而浪费一些时间你
            }
            Console.WriteLine("Count is done");
        }
View Code

运行结果如下图所示:

可调用CancellationTokenSource的Register方法登记一个或多个在取消一个CancellationTokenSource时调用的方法。

向被取消的CancellationTokenSource登记一个回调方法,将由调用Register的线程调用回调方法(如果为useSynchronizationContext参数传递了true值,就可能要通过调用线程的SynchronizationContext进行)。

多次调用Register,多个调用方法都会调用。这些回调方法可能抛出未处理的异常。

如果调用CancellationTokenSource的Cancel方法,向它传递true,那么抛出了未处理异常的第一个回调方法会阻止其他回调方法的执行,抛出的异常也会从Cancel中抛出。

如果调用Cancel并向它传递false,那么登记的所有回调方法都会调用。所有未处理的异常都会添加到一个集合中。所有回调方法都执行好后,其中任何一个抛出了未处理的异常,Cancel就会抛出一个AggregateException,该异常实例的InnerExceptions属性被设为已抛出的所有异常对象的集合。

        static void Main(string[] args)
        {
            var cts1 = new CancellationTokenSource();
            cts1.Token.Register(() => Console.WriteLine($"cts1被取消"));
            var cts2 = new CancellationTokenSource();
            cts2.Token.Register(() => Console.WriteLine($"cts2被取消"));
            var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
            linkedCts.Token.Register(() => Console.WriteLine($"linkedCts 被取消"));
            cts2.Cancel();
            Console.WriteLine($"cts1 canceled={cts1.IsCancellationRequested},cts2 canceled={cts2.IsCancellationRequested}," +
                $"linkedCts={linkedCts.IsCancellationRequested}");
            Console.ReadLine();
    }    
View Code

运行结果如下图:

如果要在过一段时间后取消操作,要么用接收延时参数的构造器构造一个CancellationTokenSource对象,要么调用CancellationTokenSource的CancelAfter方法。

五、任务 

通过观察,我们发现 ThreadPool最大的问题是没有内建的机制让你知道 操作在什么时候完成,以及操作完成时获得返回值。鉴于此,Microsoft引入了任务的概念。

下面展示一个使用task的简单例子:

        static void Main(string[] args)
        {
            Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
            //创建一个Task,现在还没有开始运行
            Task<Int32> t = new Task<int>(n => Sum((Int32)n), 10000);
            //可以后等待任务
            t.Start();
            //可选择显示等待任务完成
            t.Wait();
            //可获得结果(result属性内部会调用Wait)
            Console.WriteLine($"the Sum is:{t.Result},当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
            Console.ReadLine();
        }    
         private static Int32 Sum(Int32 n)
        {
            Int32 sum = 0;
            for (; n>0; n--)checked
            {
                sum += n; //如果n太大,会抛出System.OverflowException
            }
            Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
            return sum;
        }
View Code

运行结果如右图:

如果计算限制任务抛出未处理的异常,异常会被“吞噬”并存储到一个集合中,调用wait方法或Result属性时,这些成员会抛出一个System.AggregateException对象。

AggregateException提供了一个Handle方法,它为AggregateException中包含的每个异常都调用一个回调方法。回调方法可以为每个异常决定如何对其处理;回调返回true表示异常已处理;返回false表示未处理。调用Handle后,如果至少有一个异常没有处理,就创建一个新的AggregateException对象,其中只包含未处理的异常。

Task的静态WaitAny方法会阻塞调用线程,直到数组中的任何Task对象完成。方法返回Int32数组索引值,指明完成的是哪个Task的对象

Task的静态WaitAll方法也会阻塞调用线程,直到数组中的所有Task对象完成。

下面演示下task取消操作和task的异常处理

         static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task<Int32> t = Task.Run(() => Sum(cts.Token, 10000), cts.Token);

            cts.Cancel(); 
            try
            {
                Console.WriteLine($"the Sum is:{t.Result},当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
            }
            catch (AggregateException ex)
            {
                //将任何OperationCanceledException对象都是为已处理
                //其他任何异常都造成抛出一个新的AggregateException
                //其中只包含未处理异常
                ex.Handle(e => e is OperationCanceledException);
                Console.WriteLine("Sum was canceled");
            }
            Console.ReadLine();
        }        
         private static Int32 Sum(CancellationToken ct, Int32 n)
        {
            Int32 sum = 0;
            for (; n>0; n--)checked
            {
                //再取消标志引用的CancellationTokenSource上调用Cancel,
                //下面这行代码就会抛出OperationCanceledException
                ct.ThrowIfCancellationRequested();
                sum += n; //如果n太大,会抛出System.OverflowException
            }
            Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
            return sum;
        }
View Code

调用Wait,或者在任务尚未完成时查询任务的Result属性,极有可能造成线程池创建新线程,这增大了资源的消耗,也不利于性能和伸缩性。

要知道一个任务在什么时候结束,任务完成时可启动另一个任务。

Microsoft为我们提供了ContinueWith,下面简单展示使用

        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task<Int32> t = Task.Run(() => Sum(cts.Token, 10000), cts.Token);
            Task cwt= t.ContinueWith(task => Console.WriteLine($"Sum result is {task.Result}"));
        }        
         private static Int32 Sum(CancellationToken ct, Int32 n)
        {
            Int32 sum = 0;
            for (; n>0; n--)checked
            {
                //再取消标志引用的CancellationTokenSource上调用Cancel,
                //下面这行代码就会抛出OperationCanceledException
                ct.ThrowIfCancellationRequested();
                sum += n; //如果n太大,会抛出System.OverflowException
            }
            Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
            return sum;
        }
View Code

Task对象内部包含了ContinueWith任务的一个集合。可在调用ContinueWith时传递对一组TaskContinuationOptions枚举值进行判断满足什么情况才执行ContinueWith。

偷个懒,哈哈。。。

任务可以启动多个子任务,下面简单展示下使用

        static void Main(string[] args)
        {
            Task<Int32[]> task = new Task<Int32[]>(() =>
            {
                var results = new Int32[3];
                new Task(() => results[0] = Sum(1000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[1] = Sum(1000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[2] = Sum(1000), TaskCreationOptions.AttachedToParent).Start();
                return results;
            });
            var cwt = task.ContinueWith(
                parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
            task.Start();
            Console.ReadLine();    
        }
        private static Int32 Sum( Int32 n)
        {
            Int32 sum = 0;
            for (; n>0; n--)checked
            {
                sum += n; //如果n太大,会抛出System.OverflowException
            }
            Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
            return sum;
        }    
View Code

TaskCreationOptions.AttachedToParrent标志将一个Task和创建它的Task关联,结果是除非所有子任务(以及子任务的子任务)结束运行,否则创建任务(父任务)不认为已经结束。

在一个Task对象的存在期间,可查询Task的只读Status属性了解它在其生存期的什么位置。

 

要创建一组共享相同配置的Task对象。可创建一个任务工厂来封装通用的配置。即TaskFactory。

在调用TaskFactory或TaskFactory<TResult>的静态ContinueWhenAll和ContinueWhenAny方法,无论前置任务是如何完成的,ContinueWhenAll和ContinueWhenAny都会执行延续任务。

六、任务调度器

对于不了解任务调度的小白来讲,可能遇到过下面这个场景

啊,怎么会这样呢?为什么不能在线程里更新UI组件。

TaskScheduler对象负责执行被调度的任务,同时向Visual Studio调试器公开任务信息。

FCL提供了两个派生自TaskScheduler的类型:线程池任务调度器(thread pool task scheduler),和同步上下文任务调度器(synchronization context task scheduler)。

默认情况下,所有应用程序使用的都是线程池任务调度器。可查询TaskScheduler的静态Default属性来获得对默认任务调度器的引用。

同步上下文任务调度器适合提供了图形用户界面的应用程序。它将所有任务都调度给应用程序的GUI线程,使所有任务代码都能成功的更新UI组件。该调度不使用线程池。可执行TaskScheduler的静态FromCurrentSynchronizationContext方法来获得对同步上下文任务调度器的引用。

下面展示一个简单的例子,演示如何使用同步上下文任务调度器

     public partial class MainForm : Form
    {
        private readonly TaskScheduler m_syncContextTaskScheduler;
        public MainForm()
        {
            //获得一个对同步上下文任务调度器的引用
            m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
            Text = "Synchronization Context Task Scheduler Demo";
            Visible = true; Width = 400; Height = 400;
        }

        private CancellationTokenSource m_cts;

        protected override void OnMouseClick(MouseEventArgs e)
        {
            if(m_cts!=null)
            {
                m_cts.Cancel(); //一个操作正在运行,取消它
                m_cts = null;
            }
            else
            {
                //任务没有开始启动它
                Text = "Operation running"; 
                m_cts = new CancellationTokenSource();
                //这个任务使用默认任务调度器,在一个线程池线程上运行
                Task<Int32> t = Task.Run(()=>Sum(1000),m_cts.Token);
                //这些任务使用 同步上下文任务调度器,在GUI线程上执行
                t.ContinueWith(task => Text = "Result:" + t.Result,
                    CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion,
                    m_syncContextTaskScheduler);
                t.ContinueWith(task => Text = "Operation canceled ",
                    CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled,
                    m_syncContextTaskScheduler);
                t.ContinueWith(task => Text = "Operation defaulted ",
                    CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted,
                    m_syncContextTaskScheduler);
            }


            base.OnMouseClick(e);
        }
        private static Int32 Sum(Int32 n)
        {
            Int32 sum = 0;
            for (; n > 0; n--) checked
                {
                    sum += n; //如果n太大,会抛出System.OverflowException
                }
            return sum;
        }
    }
View Code

单击窗体的客户区域,就会在线程池线程上启动一个计算限制操作。使用线程池线程,因为GUI线程在此期间不会被阻塞,能响应其他UI操作。

 

Parallel就留在下篇来介绍吧。。。

天道酬勤,大道至简,坚持。


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
播放视频(c#)发布时间:2022-07-10
下一篇:
C#事件及事件驱动发布时间:2022-07-10
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap