简单理解设计模式——享元模式-线程池-任务(task)

最后都变了- 提交于 2020-01-30 07:12:44

前面在写到多线程的文章的时候,一直想写一篇关于线程池等一系列的文章,做一下记录,本篇博客记录一下设计模式中享元模式的设计思想,以及使用享元模式的实现案例——线程池,以及线程池的简化版——任务(task)

享元模式

在软件开发过程中,如果我们需要重复使用某个对象的时候,重复的去new这样一个对象,我们在内存中就会多次的去申请内存空间了,这样,可能会出现内存使用越来越多的情况。

如果让我们解决这个问题,会不会这样想:“既然是同一个对象,能不能只创建一个对象,然后下次需要再创建这个对象的时候,让它直接用已经创建好的对象就好了”,也就是说--让一个对象共享!

这种实现方式有点类似排版印刷术,将所有的字先提前印刷好,需要哪个字直接拿过来用,就不用每次打印字的时候再重新造一个字的模板了,这就是我理解的享元模式的思想。

享元模式的正式定义:

运用共享技术有效的支持大量细粒度的对象,享元模式可以避免大量相类似的开销,在软件开发中如果需要生成大量细粒度的类实例来表示数据,如果这些实例除了几个参数外基本都是相同的,这个时候就可以使用享元模式。如果把这些参数(指的是这是实例不同的参数,比如:排版印刷的时候每个字的位置)移动到类的外面,在调用方法时把他们传递进来,这样就通过共享数据,减少了单个实例的数目(这个也是享元模式的实现要领),我们把类实例外面的参数称之为享元对象的外部状态,把在享元模式内部定义称之为内部状态。

享元模式的实现小demo

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 享元模式
{
    class Program
    {
        static void Main(string[] args)
        {
            //定义外部状态,例如字母的位置等信息
            int externalstate = 10;
            //初始化享元工厂
            FlyweighFactory factory = new FlyweighFactory();
            //判断是否已经创建了字母A,如果已经创建就直接使用创键的对象A
            Flyweight fa = factory.GetFlyweight("A");
            if (fa != null)
            {
                //把外部状态作为享元对象的方法调用参数
                fa.Operation(--externalstate);
            }
            //判断是否已经创建了字母B
            Flyweight fb = factory.GetFlyweight("B");
            if (fb!=null)
            {
                fb.Operation(--externalstate);
            }
            //判断是否已经创建了字母C
            Flyweight fc = factory.GetFlyweight("C");
            if (fc != null)
            {
                fc.Operation(--externalstate);
            }
            //判断是否创建了字母D
            Flyweight fd = factory.GetFlyweight("D");
            if (fd != null)
            {
                fd.Operation(--externalstate);
            }
            else
            {
                Console.WriteLine("驻留池中不存在字符串D");
                //这个时候就需要创建一个对象并放入驻留池中
                ConcreteFlyweight d = new ConcreteFlyweight("D");
                factory.flyweights.Add("D", d);
            }
            Console.ReadLine();

        }
    }
    /// <summary>
    /// 享元工厂,负责创建和管理享元对象
    /// </summary>
    public class FlyweighFactory
    {
        /// <summary>
        /// 定义一个池容器
        /// </summary>
        public Hashtable flyweights = new Hashtable();
        public FlyweighFactory()
        {
            flyweights.Add("A", new ConcreteFlyweight("A"));//将对应的内部状态添加进去
            flyweights.Add("B", new ConcreteFlyweight("B"));
            flyweights.Add("C", new ConcreteFlyweight("C"));
        }
        /// <summary>
        /// 根据键来查找值
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public Flyweight GetFlyweight(string key)
        {
            return flyweights[key] as Flyweight;
        }
    }



    /// <summary>
    /// 抽象享元类,提供具体享元类具有的方法
    /// </summary>
    public abstract class Flyweight
    {
        public abstract void Operation(int extrinsicstate);
    }
    /// <summary>
    /// 具体享元对象,这样我们不把每个字符设计成一个单独的类了,而是把共享的字母作为享元对象的内部状态
    /// </summary>
    public class ConcreteFlyweight : Flyweight
    {
        /// <summary>
        /// 内部状态
        /// </summary>
        private string intrinsicstate;
        public ConcreteFlyweight(string innerState)
        {
            this.intrinsicstate = innerState;
        }
        /// <summary>
        /// 享元类的实例方法
        /// </summary>
        /// <param name="extrinsicstate">外部状态</param>
        public override void Operation(int extrinsicstate)
        {
            Console.WriteLine("具体实现类:intrinsicstate(内部状态){0},extrinsicstate(外部状态){1}", intrinsicstate, extrinsicstate);
        }
    }
}

享元模式的使用场景:

一个系统中有大量的对象;

这些对象耗费大量的内存

这些对象可以按照内部状态分成很多组,当把外部对象从对象中剔除时,每一个组都可以仅用一个对象代替

软件系统不依赖这些对象的身份。

注意:使用享元模式需要额外的维护一个记录子系统已有额所有享元的表,这也是耗费资源的。所以当在有足够多的对象实例,或者这些享元实例的创建特别耗费资源的时候可以考虑使用享元模式。

不知道你这里有没有发现,其实享元模式定义了一个“池“的概念。在排版印刷的时候,我们将所有的字(内部状态)放在一个字体池中,使用完之后将这些字(内部状态)再放回池中。

这跟我们接下来说的线程池似乎不谋而合。

线程池:

先说一下后台线程和前台线程:两者几乎相同,唯一的区别是,前台线程会阻止进程的正常退出,后台线程则不会。

线程的创建和销毁要消耗很多时间,而且过多的线程不仅会浪费内存空间,还会导致线程上下文切换频繁,影响程序性能,为改善这些问题,.Net运行时(CLR)会为每个进程开辟一个全局唯一的线程池来管理其线程。

线程池内部维护一个操作请求队列,程序执行异步操作的时候,添加目标操作到线程池的请求队列;线程池代码提取记录项并派发线程池中的一个线程;如果线程池中没有可用线程,就创建一个新的线程,创建的新线程不会随着任务的完成而销毁,这样就可以避免线程的频繁创建和销毁。如果线程池中大量的线程长时间无所事事,空闲线程会进行自我终结以释放资源。

线程池中通过保持进程中线程的少量和高效来优化程序的性能。

当线程数达到设定值且忙碌,异步任务将进入请求队列,直到有线程空闲才会执行

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace 线程池
{
    class Program
    {
        static void Main(string[] args)
        {
            RunThreadPoolDemo();
            Console.ReadLine();
        }

        static void RunThreadPoolDemo()
        {
            线程池.ThreadPoolDemo.ShowThreadPoolInfo();
            ThreadPool.SetMaxThreads(100, 100);//默认(1023,1000)(8核心CPU)
            ThreadPool.SetMinThreads(8, 8); // 默认是CPU核心数
            线程池.ThreadPoolDemo.ShowThreadPoolInfo();
            线程池.ThreadPoolDemo.MakeThreadPoolDoSomeWork(100);//计算限制任务
            线程池.ThreadPoolDemo.MakeThreadPoolDoSomeIOWork();//IO限制任务
        }
    }

    public class ThreadPoolDemo
    {
        /// <summary>
        /// 显示线程池信息
        /// </summary>
        public static void ShowThreadPoolInfo()
        {
            int workThreads, completionPortThreads;

            //当前线程池可用工作线程数量和异步IO线程数量
            ThreadPool.GetAvailableThreads(out workThreads, out completionPortThreads);
            Console.WriteLine($"GetAvailableThreads => workThreads:{0};completionPortThreads:{1}", workThreads, completionPortThreads);
            //线程池最大可用的工作线程数量和异步IO线程数量
            ThreadPool.GetMaxThreads(out workThreads, out completionPortThreads);
            Console.WriteLine($"GetMaxThreads => workThreads:{0};completionPortThreads:{1}", workThreads, completionPortThreads);
            //出现新的请求,判断是否需要创建新线程的依据
            ThreadPool.GetMinThreads(out workThreads, out completionPortThreads);
            Console.WriteLine($"GetMinThreads => workThreads:{0};completionPortThreads:{1}", workThreads, completionPortThreads);
            Console.WriteLine();

        }
        /// <summary>
        /// 让线程池做些事情
        /// </summary>
        /// <param name="workCount"></param>
        public static void MakeThreadPoolDoSomeWork(int workCount = 10)
        {
            for (int i = 0; i < workCount; i++)
            {
                int index = i;
                ThreadPool.QueueUserWorkItem(s =>
                {
                    Thread.Sleep(100);//模拟工作时长
                    Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{index}]");
                    ShowAvailableThreads("WorkerThread");
                });
            }
        }

        /// <summary>
        /// 让线程做一些IO工作
        /// </summary>
        public static void MakeThreadPoolDoSomeIOWork()
        {
            //随便找一些可以访问的网址
            IList<string> urlList = new List<string>()
            {
                "http://news.baidu.com/",
                "https://www.hao123.com/",
                "https://map.baidu.com/",
                "https://tieba.baidu.com/",
                "https://wenku.baidu.com/",
                "http://fanyi-pro.baidu.com",
                "http://bit.baidu.com/",
                "http://xueshu.baidu.com/",
                "http://www.cnki.net/",
                "http://www.wanfangdata.com.cn",
            };

            foreach (var uri in urlList)
            {
                WebRequest request = WebRequest.Create(uri);
                //request包含此异步请求的状态信息的对象
                request.BeginGetResponse(ac =>
                {
                    try
                    {
                        WebResponse response = request.EndGetResponse(ac);
                        ShowAvailableThreads("IOThread");
                        Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{response.ContentLength}]");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                },request);
            }
        }

        /// <summary>
        /// 打印线程池可用线程
        /// </summary>
        /// <param name="sourceTag"></param>
        private static void ShowAvailableThreads(string sourceTag = null)
        {
            int workThreads, completionPortThreads;
            ThreadPool.GetAvailableThreads(out workThreads, out completionPortThreads);
            Console.WriteLine($"{0} GetAvailableThreads => workThreads:{1};completionPortThreads:{2}",sourceTag,workThreads,completionPortThreads);
            Console.WriteLine();
        }

        /// <summary>
        /// 取消通知者
        /// </summary>
        public static CancellationTokenSource CTSource { get; set; } = new CancellationTokenSource();

        /// <summary>
        /// 执行可取消的任务
        /// </summary>
        public static void DoSomeWorkWithCancellation()
        {
            ThreadPool.QueueUserWorkItem(t =>
            {
                Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] begun running. [0 - 9999]");

                for (int i = 0; i < 10000; i++)
                {
                    if (CTSource.Token.IsCancellationRequested)
                    {
                        Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] recived the cancel token. [{i}]");
                        break;
                    }
                    Thread.Sleep(100);//模拟工作时长
                }
                Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] was cancelled.");
            });
        }
    }
}

代码中含有中文命名空间,这样写不规范,请不要模仿~

线程池内部维护着一个工作项队列,这个队列指的是线程池的全局队列,实际上,除了全局队列,线程池会给每个工作者线程维护一个本地队列

当我们调用ThreadPool.QueueUserWorkItem方法时,工作项会被放入全局队列;使用定时器Timer的时候,也会将工作项放入全局队列;但是,当我们使用任务Task的时候,假如使用默认的任务调度器,任务会被调度到工作者线程的本地队列中。

工作者线程优先执行本地队列中最新进入的任务,如果本地队列中已经没有任务,线程会尝试从其他工作者线程任务队列的队尾取任务执行,这里需要进行同步。如果所有工作者线程的本地队列都没有任务可以执行,工作者线程才会从全局队列取最新的工作项来执行。所有任务执行完毕后,线程睡眠,睡眠一定时间后,线程醒来并销毁自己以释放资源

异步IO实现过程如下:

  1. 托管的IO请求线程调用Win32本地代码ReadFile方法
  2. ReadFile方法分配IO请求包IRP并发送至Windows内核
  3. Windows内核把收到的IRP放入对应设备驱动程序的IRP队列中,此时IO请求线程已经可以返回托管代码
  4. 驱动程序处理IRP并将处理结果放入.NET线程池的IRP结果队列中
  5. 线程池分配IO线程处理IRP结果

任务(Task)

我理解的任务是在线程池的基础上进行的优化,但是任务比线程有更小的开销和更精确的控制,任务是架构在线程之上的,就是说,任务最后还是抛给线程去执行。

开10个任务,并不会开是个线程,这是我理解的再线程池的基础上优化的依据。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Tesk_任务_
{
    class Program
    {
        static void Main(string[] args)
        {

            #region 创建任务
            //第一种方式开一个任务
            Task t = new Task(() =>
            {
                Console.WriteLine("任务工作开始......");
                //模拟工作过程
                Thread.Sleep(5000);
            });
            t.Start();
            //第二种方式创建任务
            Task t = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("任务工作开始......");
                Thread.Sleep(5000);
            });
            //当第一的任务工作完成之后接着执行这一步操作
            t.ContinueWith((task) =>
            {
                Console.WriteLine("任务完成,完成时的状态为:");
                Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
            });
            Console.WriteLine("等待任务完成!");
            Console.ReadKey();
            #endregion

            #region 任务的生命周期
            var task1 = new Task(() =>
            {
                Console.WriteLine("Begin");
                Thread.Sleep(2000);
                Console.WriteLine("Finish");
            });
            Console.WriteLine("Begin start:" + task1.Status);
            task1.Start();//开启任务
            Console.WriteLine("After start:" + task1.Status);
            task1.Wait();
            Console.WriteLine("After Finsh:" + task1.Status);
            Console.ReadLine();
            #endregion

            #region Task的任务控制
            var task1 = new Task(() =>
                {
                    Console.WriteLine("Begin1");
                    Thread.Sleep(2000);
                    Console.WriteLine("Finish1");
                });
            var task2 = new Task(() =>
            {
                Console.WriteLine("Begin2");
                Thread.Sleep(5000);
                Console.WriteLine("Finish2");
            });

            task1.Start();//开启任务
            task2.Start();//开启第二个任务
            //public Task ContinueWith(Action<Task> continuationAction);
            //ContinueWith<string>:string是这个任务的返回值类型
            var result = task1.ContinueWith<string>(task =>
            {
                Console.WriteLine("task1 finished");
                return "this is task result";
            });
            //task1.Wait();//等待第一个任务完成
            //Task.WaitAll(task1, task2);
            //Console.WriteLine("All task Finshed:");
            Console.WriteLine(result.Result.ToString());
            Console.ReadLine();

            //通过ContinueWith获取第一个任务的返回值
            var a = Task.Factory.StartNew(() => { return "One"; }).ContinueWith<string>(ss => { return ss.Result.ToString(); });

            Task b = new Task<string>(() =>
            {
                return "one";
            });
            Console.WriteLine(b.ToString());//这样获取不到b任务的返回值

            Console.WriteLine(a.Result);



            #region TaskContinuationOptions 定义延续任务在什么情况下执行
            Task<Int32> t = new Task<Int32>(i => Sum((Int32)i), 10000);
            t.Start();
            //TaskContinuationOptions创建延续任务的行为,OnlyOnRanToCompletion只有当前面的任务执行完才能安排延续任务
            t.ContinueWith(task => Console.WriteLine("The sum is:{0}", task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);

            //OnlyOnFaulted延续任务前面的任务出现了异常才会安排延续任务,将任务中的错误信息打印出来了
            t.ContinueWith(task => Console.WriteLine("Sum throw:{0}", task.Exception), TaskContinuationOptions.OnlyOnFaulted);
            //OnlyOnCanceled延续任务前面的任务已取消的情况下才会安排延续任务
            t.ContinueWith(task => Console.WriteLine("Sum was cancel:{0}", task.IsCanceled), TaskContinuationOptions.OnlyOnCanceled);
            try
            {
                t.Wait();
            }
            catch (AggregateException)
            {

                Console.WriteLine("出错");
            }
            #endregion

            #region AttachedToParnt枚举类型(父任务)
            Task<Int32[]> parent = new Task<int[]>(() =>
            {
                var results = new Int32[3];
                new Task(() => results[0] = Sum(1000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[1] = Sum(2000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[2] = Sum(3000), TaskCreationOptions.AttachedToParent).Start();
                return results;
            });
            //任务返回的是一个数组,我要做的是对数组进行打印ForEach(),
            var cwt = parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
            parent.Start();
            cwt.Wait();
            #endregion

            #region 取消任务
            CancellationTokenSource cts = new CancellationTokenSource();
            Task<Int32> t = new Task<int>(() => Sum(cts.Token, 1000), cts.Token);
            //可以现在开始,也可以以后开始
            t.Start();
            //在之后的某个时间,取消CancellationTokenSource 以取消Task
            cts.Cancel();//这个是异步请求,Task可能已经完成了
            //注释这个为了测试抛出的异常
            //Console.WriteLine("This sum is:", t.Result);
            try
            {
                //如果任务已经取消了,Result会抛出AggregateException
                Console.WriteLine("This sum is:", t.Result);
            }
            catch (AggregateException x)
            {
                x.Handle(e => e is OperationCanceledException);
                Console.WriteLine("Sum was Canceled");
                
            }

            #endregion
            Console.ReadLine();
            #endregion
        }


        private static Int32 Sum(Int32 i)
        {
            Int32 sum = 0;
            for (; i >0 ; i--)
            {
                checked { sum += i; }
            }
            return sum;
        }
        private static Int32 Sum(CancellationToken ct, Int32 i)
        {
            Int32 sum = 0;
            for (; i >0; i--)
            {
                //在取消标志引用的CancellationTokenSource上如果调用
                //Cancel,下面这一行就会抛出OperationCanceledException
                ct.ThrowIfCancellationRequested();
                checked { sum += i; }
            }
            return sum;
        }
    }
}

注意:这里的代码并不是复制就可以执行的,之前做demo测试的时候将所有的代码都糅杂在一起了!

关于技术与业务我也纠结过一段时间,是业务重要还是技术重要,后来发现,技术是服务于业务的,设计模式是技术吗?其实它是为了解决某种实现场景总结出来的。业务和技术应该是相辅相成的,在工作中难免会遇到一些重复性的工作,可不可以尝试着改进在工作中的实现方式来提高自己的技术水平呢?加油~ 追梦人!

参考文章:

https://www.cnblogs.com/chenbaoshun/p/10566124.html

https://www.cnblogs.com/zhili/p/FlyweightPattern.html

设计模式相关网页:

https://www.cnblogs.com/caoyc/p/6927092.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!