7種建立線程方式,你知道幾種?線程系列Thread(一)

前言

最近特別忙,博客就此荒蕪,博主秉着哪裏不熟悉就開始學習哪裏的精神一直在分享着,有着紮實的基礎才能寫出健壯的代碼,有可能實現的邏輯有多種,可是心中必須有要有底哪一個更適合,用着更好,不然則說明咱們對這方面還比較薄弱,這個時候就得好好補補了,這樣才能加快提高自身能力的步伐,接下來的時間會着重講解線程方面的知識。強勢分割線。面試

 


 

話題亂入,一到跳槽季節想必咱們不少人就開始刷面試題,這種狀況下大部分都能解決問題,可是這樣的結果則是致使有可能企業招到並不是合適的人,固然做爲面試官的那些人們也懶得再去本身出一份面試題,問來問去就那些技術【排除有些裝逼的面試官】,若是我做爲面試官我會在網上挑出50%的面試題,其餘面試則是現場問答,看看面試者的實際能力和平時的積累是怎樣的。好了,如今隨便出三道面試題,做爲面試者的你,看你如何做答:windows

(1)利用Thread類建立線程有幾種方式。api

(2)若是你已工做3年,我要問你建立線程的至少3種方式,若是你已工做6年,我會問你建立線程的7種方式。瀏覽器

(3)線程的發展歷程是怎樣的,每個歷程分別是爲了解決什麼問題。安全

若是你須要沉思一會或者回答不出來,那你就有必要好好補補線程這方面的知識了!若是答案已有請對照文章最底部參考答案是否大概一致。cookie

線程

線程確實很強大,強大到對於我而言只知道這個概念,因爲自身的能力沒法從底層去追究,只能經過網上資料或書籍來強勢入腦,可是利用線程不當則致使各類各樣問題的出現,若不做爲開發者咱們只能重啓電腦或者打開任務管理器去直接關閉該死的那所屬的進程,做爲開發者的咱們知道線程有着內存佔用和運行時的性能開銷即建立和銷燬都是須要開銷。每一個線程都有如下因素多線程

(1)線程內核對象。併發

(2)線程環境塊。異步

(3)用戶模式棧。函數

(4)內核模式棧。

(5)DLL線程鏈接和線程分離通知。

上述摘抄來自CLR Via  C#,請原諒我懶得去看這段文字也不想看,沒多大意思【由於我不懂】,比較底層的東西我就不去過多探討了。好了,開始進入咱們最原始的線程建立講解。

線程基礎(Thread)

咱們建立一個線程並執行對應方法,以下:

            var t = new Thread(Basic);
            t.Start();

            static void Basic()
            {
                Console.WriteLine("跟着Jeffcky學習線程系列");
            }

就是這麼簡單, 該線程實例有一個 IsAlive 屬性,一旦線程啓動該屬性則會爲True直到線程執行完畢。接下來咱們將上述再添加一句打印以下:

            var t = new Thread(Basic);
            t.Start();
            Console.WriteLine("我是主線程");

固然也有多是這樣的

在主線程上建立了一個新的線程,此時雖然建立了新的線程可是還未就緒,主線程搶先一步而執行。致使打印前後順序就不一樣。下面咱們再來看一個例子:

    class Program
    {
        static bool isRun;
        static void Main(string[] args)
        {
            var t = new Thread(Basic);
            t.Start();
            Basic();
            Console.ReadKey();
        }

        static void Basic()
        {
            if (!isRun)
            {
                Console.WriteLine("正在運行");
                isRun = true;
            }
        }
    }

此時你以爲結果可能會是這樣的,是否是必定是以下這樣呢?

若是咱們再多運行幾回,你會發現出現以下結果:

爲何會出現兩種大相徑庭的結果,這裏就得涉及到線程安全的問題,這裏兩個線程就屬於多線程場景,有可能當主線程或者建立的線程先執行打印出【正在執行】,此時將isRun設置爲True,而這個時候主線程或者新線程才執行到這個Basic,此時isRun已經爲True,那麼將只能打印一次。若是將上述代碼進行以下改造,只打印出一個的機率將會大大提升。

        static void Basic()
        {
            if (!isRun)
            {
                isRun = true;
                Console.WriteLine("正在運行");
            }
        }

此時爲了保證在控制檯中只打印一次,咱們須要採用加鎖機制,以下:

    class Program
    {
        static bool isRun;
        static readonly object objectLocker = new object();
        static void Main(string[] args)
        {
            var t = new Thread(Basic);
            t.Start();
            Basic();
            Console.ReadKey();
        }

        static void Basic()
        {
            lock (objectLocker)
            {
                if (!isRun)
                {
                    isRun = true;
                    Console.WriteLine("正在運行");
                }
            }
        }
    }

咱們看看Thread這個類中建立線程的構造函數,看到建立線程有以下兩個構造函數:

        //
        // 摘要:
        //     初始化 System.Threading.Thread 類的新實例。
        //
        // 參數:
        //   start:
        //     表示開始執行此線程時要調用的方法的 System.Threading.ThreadStart 委託。
        //
        // 異常:
        //   T:System.ArgumentNullException:
        //     start 參數爲 null。
        [SecuritySafeCritical]
        public Thread(ThreadStart start);
        //
        // 摘要:
        //     初始化 System.Threading.Thread 類的新實例,指定容許對象在線程啓動時傳遞給線程的委託。
        //
        // 參數:
        //   start:
        //     一個委託,它表示此線程開始執行時要調用的方法。
        //
        // 異常:
        //   T:System.ArgumentNullException:
        //     start 爲 null。
        [SecuritySafeCritical]
        public Thread(ParameterizedThreadStart start);

咱們簡單過一下

 var t = new Thread(new ThreadStart(Basic));

第二個構造函數中的參數爲一個委託類型,以下:

    [ComVisible(false)]
    public delegate void ParameterizedThreadStart(object obj);

這個時候就明朗了,在沒有lambda表達式出現前,咱們只能經過匿名方法來實現。

            var t = new Thread(delegate () { Basic(); });
            t.Start();

有了lambda出現,建立線程注入參數則更加簡便了,以下:

            var t = new Thread(()=> { Basic(); });
            t.Start();

固然根據上述委託定義,咱們一樣可以傳遞參數,以下:

         var t = new Thread(()=> { Basic("Hello cnblogs"); });
         t.Start();

        static void Basic(string message)
        {
            Console.WriteLine(message);
        }    

同時咱們看到啓動線程的方法Start還有以下參數爲object的重載。

此時咱們還能夠經過Start來傳遞委託參數,以下:

         var t = new Thread(()=> { Basic });
         t.Start("Hello cnblogs");

        static void Basic(object message)
        {
            var msg = message as string;
            Console.WriteLine(message);
        }    

好了到了這裏咱們解決了第一道面試題,經過Thread建立線程有如上四種方式(確切的說是兩種不一樣方式,四種表現形式)。有時候咱們在多線程場景下須要阻塞主線程而等待建立的線程的結果再往下執行,此時咱們須要用到JOIN和Sleep來進行阻塞。

線程基礎(JOIN和Sleep)

有時候咱們須要等待上一線程執行完畢獲得其結果接着往下進行,此時咱們能夠經過線程中的JOIN和Sleep來阻塞當前線程,以下所示由於Main方法調用JOIN方法,那麼JOIN方法會形成調用線程阻塞當前執行的任何代碼等待新建立線程的銷燬或終止才繼續往下執行。

    class Program
    {
        static void Main(string[] args)
        {
            var t = new Thread(Basic);
            t.Start("Hello cnblogs");
            t.Join();
            Console.WriteLine("我是主線程");
            Console.ReadKey();
        }

        static void Basic(object message)
        {
            var msg = message as string;
            Console.WriteLine(message);
        }
    }

一樣利用Sleep也是如此

            var t = new Thread(Basic);
            t.Start("Hello cnblogs");
            Thread.Sleep(4000);
            Console.WriteLine("我是主線程");

同時咱們應該看到Sleep方法有以下說明:

也就是說用Thread.Sleep(0)會當即釋放當前時間片,讓出cpu來執行其餘線程,此時就有可能打印出主線程和新線程的順序前後不同。

線程基礎(進程和線程)

講到線程咱們就離不開對進程的講解,線程被稱爲輕量級進程,它是cpu執行的最小單元,而進程是操做系統執行的基本單元,一個進程能夠包含多個線程,在任務管理器咱們看到的則是進程,每一個進程之間相互獨立,各自爲政,這個稍微想象一下就能明白,如有影響那就亂套了,究其根本緣由則是,每一個進程都被賦予了一塊虛擬地址空間,這樣就確保在一個進程中使用的代碼和數據沒法由另一個進程訪問,但線程與線程之間就不必定,線程與線程之間能夠共享內存,這個理解起來也不難,當咱們一個線程在獲取數據時,此時另外一個線程則能夠顯示去獲取數據進程內存中所存放的數據。那麼問題又來了,線程究竟是如何工做的呢?就像一場活動,總有主辦方來安排這一切,來的客人一進門都會被工做人員安排會座位並被好生招牌,如此一切才能井井有理進行,此時的客戶就像一個線程,因此同理,在線程內部有一個線程調度器來安排線程的幾個狀態,好比活動主辦方請客戶過來觀看,此時就有一個帖子上面寫好了邀請的人,這就像線程中的狀態之一【新建】,當主辦方一切安排穩當活動開始後,此時會邀請客戶到上面去演講,上一個快要演講完畢此時會通知下一位,此時就像線程狀態之二【就緒】,最後輪到客戶上去演講,很天然就過渡到了線程狀態之三【運行】,在客戶演講時中途可能還有答問環節才能繼續進行下一環節的繼續進行,此時就像線程狀態之四【阻塞】,最終客戶演講完畢,主辦方會送客戶離場,此時客戶的任務算是結束,這就像線程最終狀態【死亡】,如此就完成了一個線程的整個生命週期。線程調度器會確保當前全部線程都可以分配到合適的時間,就像人民名義中侯亮平對全部人都一視同仁,毫不徇私。若是一個線程在等待一個用戶的操做,在一個時間片的長度內用戶沒有徹底用完,也就說用戶沒有進行持續輸入,那麼此時線程將進入等待狀態,剩餘的時間片將自動進行放棄,使得在任何cpu上都不會執行該線程,直到發生下一次輸入事件,因此在總體上加強了系統的性能,由於其線程可自動終止其時間片,因此調用線程的線程調度器在必定程度上保證那些被阻塞的線程不會消耗cpu時間。

 

那麼問題來了,當一個線程的時間片用完,操做系統將進行上下文切換(windows操做系統大約30毫秒執行一次上下文切換),那麼進行上下文切換時到底發生了什麼呢?

這個時候咱們就有必要了解線程的組成部分:一個標準的線程由線程ID,當前指令指針,寄存器組合和堆棧-來源(http://baike.sogou.com/v49119.htm?fromTitle=線程##5)那麼再下次獲取上一次線程用戶輸入的值須要通過如下三個階段。

(1)將cpu寄存器中的值保存到當前正在運行的線程的內核對象內部的一個上下文結構中。

(2)從現有線程集合中選出一個線程供調度,若是該線程由另一個進程擁有,windows在開始執行任何代碼或者接觸任何數據以前,還必須切換cpu可以看見的虛擬地址空間。

(3)將全部上下文結構中的值加載到cpu的寄存器中。

線程基礎(前臺線程和後臺線程)

默認狀況下經過Thread建立的線程都爲前臺線程,若是咱們須要顯式指定建立的線程爲後臺線程,此時咱們須要進行以下指定。

            var t = new Thread(Basic);
            t.IsBackground = true;
            t.Start("Hello cnblogs");           
            Console.WriteLine("我是主線程");

上述咱們將建立的線程改寫爲後臺線程,一旦前臺線程即主線程執行完畢,此時那麼後臺線程也隨即結束,接下來咱們進行以下改造。

            var str = string.Empty;
            var t = new Thread(() => Console.WriteLine());
            if (str.Length > 0)
            {
                t.IsBackground = true;
            }

如上咱們知道str長度爲0此時也就說明建立的新線程爲前臺線程,即便此時主線程結束了,可是建立的新線程會依然賴活着,關於前臺線程一旦結束則全部後臺也會強制結束,然後臺線程結束並不會致使前臺線程自動結束,這個也不難理解,好比在瀏覽器上多開幾個頁面,此時在後臺也會建立對應的打開的tab線程,可是如果關閉這個tab頁只是關閉了建立這個tab的後臺線程而前臺線程即瀏覽器不會關閉,若關閉瀏覽器的線程此時全部打開頁的後臺線程將強制進行結束就是這麼個緣由。

線程基礎(異常處理) 

咱們來看下程序:

    class Program
    {
        static void Main(string[] args)
        {            
            try
            {
                var t = new Thread(Basic);
                t.Start();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
               
            Console.ReadKey();
        }

        static void Basic()
        {
            throw null;
        }
    }

咱們會發現對上述執行方法try{}catch{}結果永遠都不會拋異常,這是由於線程有其獨立的執行路徑,因此在當前線程上不會拋出異常,因此咱們只能在方法內部去拋出異常並解析,以下:

        static void Basic()
        {
            try
            {
                throw null;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

線程基礎(優先級)

咱們稍微過一下線程的優先級,線程優先級有以下幾個枚舉值。

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

爲什麼要出現線程優先級,咱們想一想若是是在多線程場景下咱們有可能明確須要某個線程的優先級很高,讓其優先執行,因此在多線程下線程優先級頗有意義,可是實際狀況下不多有開發者去設置這個屬性,那是爲何呢,此時咱們得講講優先級了,線程優先級有0(最低)-31(最高)之間的值,操做系統決定讓cpu去執行哪一個線程時首先會去檢查線程的優先級,經過優先級來採起輪流的方式調度線程以此類推,可是這其中就存在一個問題,若是將一個線程優先級設置爲31,那麼系統將永遠不會對0-30的線程分配cpu,如此則形成【線程飢餓】,就像簽定了霸王條款同樣,致使優先級高的線程長期佔用cpu,那麼其餘的線程處於空閒則沒法充分利用cpu,因此對於優先級的設置誰會去幹呢。

多線程

保持UI界面持續響應

當有工做線程須要執行很長時間時,此時用多線程依然能夠保持鍵盤和鼠標的事件。

並行計算

若是須要執行許多任務時,此時利用多線程採用分治策略將任務進行分攤,此時會提升計算效率。

充分利用cpu

當執行任務時此時有線程出現阻塞狀態,此時利用多線程則可以充分利用已經被空閒無所事事的線程。

同時處理多個請求

若是客戶端出現併發同時來多個請求,此時咱們利用多線程則可以徹底處理這樣的狀況。

總結

本文只是做爲線程系列開胃菜,接下來咱們將講述線程池以及線程同步構造,內容開端的答案是否已經準備好呢,咱們一一來解答。

(1)上述已經給出答案

(2)建立線程的7種方式以下:

    class Program
    {
        static BackgroundWorker bw = new BackgroundWorker();
        static void Main(string[] args)
        {
            //線程實現方式一
            var t = new Thread(Basic);
            t.Start();

            //線程實現方式二
            bw.DoWork += bw_basic;
            bw.RunWorkerAsync("Jeffcky from cnblogs");

            //線程實現方式三
            ThreadPool.QueueUserWorkItem(Basic);

            //線程實現方式四
            Func<string, int> method = RetLength;
            IAsyncResult cookie = method.BeginInvoke("Jeffcky", null, null);
            int result = method.EndInvoke(cookie);

            //線程實現方式五
            new Task(Basic, 23).Start();

            //線程實現方式六
            Task.Run(() => Basic(23));

            //線程實現方式七
            Task.Factory.StartNew(() => Basic(23));

            Console.ReadKey();
        }

        static void bw_basic(object sender, DoWorkEventArgs e)
        {
            Console.WriteLine(e.Argument);
        }

        static void Basic(object message)
        {
            var msg = (string)message;
            Console.WriteLine(message);
        }

        static int RetLength(string str)
        {
            return str.Length;
        }
    }

(3)線程歷程

Thread:雖說是有CLR來管理但實際上可等同於Windows線程,咱們能夠看所是操做系統級別線程,有它的堆棧和核心資源,雖然有豐富的api咱們能夠設置其運行狀態和優先級可是其性能開銷之大可想而知,每一個線程的建立都要消耗沒記錯的話應該是1兆的內存,同時對於線程進行上下文的切換額外還增長了cpu的開銷,若是線程不夠處理當前請求還得從新建立線程同時咱們還得手動去維護線程的狀態。

 

ThreadPool:線程池這才正式由CLR管理,線程池就像線程的包裝器,它沒有任何控制,咱們能夠隨時來提交咱們須要執行的工做,咱們能夠控制線程池的大小來優化性能,咱們不須要再額外設置其餘內容,咱們不須要告訴線程什麼時候開始執行咱們的任務,在CLR初始化時,線程池中沒有任何線程,在線程池內部維護了一個操做請求隊列,當程序執行操做時,此時會將該任務追加到線程池的隊列中,當到要執行的線程池隊列中的線程時,此時從隊列中取出並將任務派發給已取出隊列中的線程,當線程池中的線程執行完任務後此時線程將不會被銷燬,它會從新返回到線程池中並處於空閒狀態,等待下一個請求的調度,因此因爲線程不會自身進行銷燬而是進行回收,不會再產生額外的性能損失,固然建立線程會形成必定的性能損失這是不可避免的,可是利用線程池來執行任務最適合哪些不須要通知結果的操做,若是咱們須要明確知道操做何時完成而且有返回值,那麼此時線程池就作不到。

 

Task:該TPL提供了足夠豐富的api而且像線程池同樣不會建立本身的操做系統級別線程,經過Task咱們能夠查找到任務什麼時候完成而且能夠在現有任務基礎上進行ContinueWith,同時咱們能夠經過Wait來同步等待其結果就像Thread中的JOIN方法同樣,因爲任務依然是在線程池上執行,因此不適合執行長時間的任務操做,由於任務能夠填充線程池來阻塞新的任務,Task提供了一個LongRunning選項來告知不運行在線程池上。全部最新的高級併發api,如Parallel.For *()方法,PLINQ,C#5等待以及BCL中的現代異步方法都是基於Task構建的。

綜上所述,咱們能夠得出一個結論:Thread爲操做系統級別線程,建立線程以及上下文切換帶來的巨大性能開銷可想而知,致使死鎖的狀況更是沒法想象,利用ThreadPool來對線程進行回收不會再形成上下文切換的性能損失,可是它沒法告知任務執行的結果,經過Task在線程池的基礎上實現任務執行完成的結果並在現有任務上進行其餘操做以及其餘對於併發的高級api讓咱們再次歡喜,成爲.net開發者的福音。

相關文章
相關標籤/搜索