簡單理解設計模式——享元模式-線程池-任務(task)


前面在寫到多線程的文章的時候,一直想寫一篇關於線程池等一系列的文章,做一下記錄,本篇博客記錄一下設計模式中享元模式的設計思想,以及使用享元模式的實現案例——線程池,以及線程池的簡化版——任務(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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM