1. 程式人生 > >簡單理解設計模式——享元模式-執行緒池-任務(task)

簡單理解設計模式——享元模式-執行緒池-任務(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

&n