1. 程式人生 > >學習筆記-多執行緒程式設計與執行緒同步

學習筆記-多執行緒程式設計與執行緒同步

 

  執行緒基礎

微笑 程序與執行緒

我們執行一個exe,就是一個程序例項,系統中有很多個程序。每一個程序都有自己的記憶體地址空間,每個程序相當於一個獨立的邊界,有自己的獨佔的資源,程序之間不能共享程式碼和資料空間。

image

每一個程序有一個或多個執行緒,程序內多個執行緒可以共享所屬程序的資源和資料,執行緒是作業系統排程的基本單元。執行緒是由作業系統來排程和執行的,她的基本狀態如下圖。

image

微笑 執行緒的開銷及排程

當我們建立了一個執行緒後,執行緒裡面到底有些什麼東西呢?主要包括執行緒核心物件、執行緒環境塊、1M大小的使用者模式棧、核心模式棧。其中使用者模式棧對於普通的系統執行緒那1M是預留的,在需要的時候才會分配,但是對於CLR執行緒,那1M是一開始就分類了記憶體空間的。

補充一句,CLR執行緒是直接對應於一個Windows執行緒的。

image

還記得以前學校裡學習計算機課程裡講到,計算機的核心計算資源就是CPU核心和CPU暫存器,這也就是執行緒執行的主要戰場。作業系統中那麼多執行緒(一般都有上千個執行緒,大部分都處於休眠狀態),對於單核CPU,一次只能有一個執行緒被排程執行,那麼多執行緒怎麼分配的呢?Windows系統採用時間輪詢機制,CPU計算資源以時間片(大約30ms)的形式分配給執行執行緒。

計算雞資源(CPU核心和CPU暫存器)一次只能排程一個執行緒,具體的排程流程:

  • 把CPU暫存器內的資料儲存到當前執行緒內部(執行緒上下文等地方),給下一個執行緒騰地方;
  • 執行緒排程:線上程集合裡取出一個需要執行的執行緒;
  • 載入新執行緒的上下文資料到CPU暫存器;
  • 新執行緒執行,享受她自己的CPU時間片(大約30ms),完了之後繼續回到第一步,繼續輪迴;

上面執行緒排程的過程,就是一次執行緒切換,一次切換就涉及到執行緒上下文等資料的搬入搬出,效能開銷是很大的。因此執行緒不可濫用,執行緒的建立和消費也是很昂貴的,這也是為什麼建議儘量使用執行緒池的一個主要原因。

對於Thread的使用太簡單了,這裡就不重複了,總結一下執行緒的主要幾點效能影響

  • 執行緒的建立、銷燬都是很昂貴的;
  • 執行緒上下文切換有極大的效能開銷,當然假如需要排程的新執行緒與當前是同一執行緒的話,就不需要執行緒上下文切換了,效率要快很多;
  • 這一點需要注意,GC執行回收時,首先要(安全的)掛起所有執行緒,遍歷所有執行緒棧(根),GC回收後更新所有執行緒的根地址,再恢復執行緒呼叫,執行緒越多,GC要乾的活就越多;

當然現在硬體的發展,CPU的核心越來越多,多執行緒技術可以極大提高應用程式的效率。但這也必須在合理利用多執行緒技術的前提下,了執行緒的基本原理,然後根據實際需求,還要注意相關資源環境,如磁碟IO、網路等情況綜合考慮。

  多執行緒

單執行緒的使用這裡就略過了,那太easy了。上面總結了執行緒的諸多不足,因此微軟提供了可供多執行緒程式設計的各種技術,如執行緒池、任務、並行等等。

微笑 執行緒池ThreadPool

執行緒池的使用是非常簡單的,如下面的程式碼,把需要執行的程式碼提交到執行緒池,執行緒池內部會安排一個空閒的執行緒來執行你的程式碼,完全不用管理內部是如何進行執行緒排程的。

ThreadPool.QueueUserWorkItem(t => Console.WriteLine("Hello thread pool"));

每個CLR都有一個執行緒池,執行緒池在CLR內可以多個AppDomain共享,執行緒池是CLR內部管理的一個執行緒集合,初始是沒有執行緒的,在需要的時候才會建立。執行緒池的主要結構圖如下圖所示,基本流程如下:

  • 執行緒池內部維護一個請求列隊,用於快取使用者請求需要執行的程式碼任務,就是ThreadPool.QueueUserWorkItem提交的請求;
  • 有新任務後,執行緒池使用空閒執行緒或新執行緒來執行佇列請求;
  • 任務執行完後執行緒不會銷燬,留著重複使用;
  • 執行緒池自己負責維護執行緒的建立和銷燬,當執行緒池中有大量閒置的執行緒時,執行緒池會自動結束一部分多餘的執行緒來釋放資源;

執行緒池是有一個容量的,因為他是一個池子嘛,可以設定執行緒池的最大活躍執行緒數,呼叫方法ThreadPool.SetMaxThreads可以設定相關引數。但很多程式設計實踐裡都不建議程式猿們自己去設定這些引數,其實微軟為了提高執行緒池效能,做了大量的優化,執行緒池可以很智慧的確定是否要建立或是消費執行緒,大多數情況都可以滿足需求了。

執行緒池使得執行緒可以充分有效地被利用,減少了任務啟動的延遲,也不用大量的去建立執行緒,避免了大量執行緒的建立和銷燬對效能的極大影響。

上面瞭解了執行緒的基本原理和諸多優點後,如果你是一個愛思考的猿類,應該會很容易發現很多疑問,比如把任務新增到執行緒池佇列後,怎麼取消或掛起呢?如何知道她執行完了呢?下面來總結一下執行緒池的不足:

  • 執行緒池內的執行緒不支援執行緒的掛起、取消等操作,如想要取消執行緒裡的任務,.NET支援一種協作式方式取消,使用起來也不少很方便,而且有些場景並不滿足需求;
  • 執行緒內的任務沒有返回值,也不知道何時執行完成;
  • 不支援設定執行緒的優先順序,還包括其他類似需要對執行緒有更多的控制的需求都不支援;

因此微軟為我們提供了另外一個東西叫做Task來補充執行緒池的某些不足。

大笑 任務Task與並行Parallel

任務Task與並行Parallel本質上內部都是使用的執行緒池,提供了更豐富的並行程式設計的方式。任務Task基於執行緒池,可支援返回值,支援比較強大的任務執行計劃定製等功能,下面是一個簡單的示例。Task提供了很多方法和屬性,通過這些方法和屬效能夠對Task的執行進行控制,並且能夠獲得其狀態資訊。Task的建立和執行都是獨立的,因此可以對關聯操作的執行擁有完全的控制權。

//建立一個任務
Task<int> t1 = new Task<int>(n =>
{
    System.Threading.Thread.Sleep(1000);
    return (int)n;
}, 1000);
//定製一個延續任務計劃
t1.ContinueWith(task =>
{
    Console.WriteLine("end" + t1.Result);
}, TaskContinuationOptions.AttachedToParent);
t1.Start();
//使用Task.Factory建立並啟動一個任務
var t2 = System.Threading.Tasks.Task.Factory.StartNew(() =>
{
    Console.WriteLine("t1:" + t1.Status);
});
Task.WaitAll();
Console.WriteLine(t1.Result);

並行Parallel內部其實使用的是Task物件(TPL會在內部建立System.Threading.Tasks.Task的例項),所有並行任務完成後才會返回。少量短時間任務建議就不要使用並行Parallel了,並行Parallel本身也是有效能開銷的,而且還要進行並行任務排程、建立呼叫方法的委託等等。

馬上回來 GUI執行緒處理模型

這是很多開發C/S客戶端應用程式會遇到的問題,GUI程式的介面控制元件不允許跨執行緒訪問,如果在其他執行緒中訪問了介面控制元件,執行時就會丟擲一個異常,就像下面的圖示,是不是很熟悉!這其中的罪魁禍首就是,就是“GUI的執行緒處理模型”。

image

.NET支援多種不同應用程式模型,大多數的執行緒都是可以做任何事情(他們可能沒有引入執行緒模型),但GUI應用程式(主要是Winform、WPF)引入了一個特殊執行緒處理模型,UI控制元件元素只能由建立它的執行緒訪問或修改,微軟這樣處理是為了保證UI控制元件的執行緒安全。

為什麼在UI執行緒中執行一個耗時的計算操作,會導致UI假死呢?這個問題要追溯到Windows的訊息機制了。

因為Windows是基於訊息機制的,我們在UI上所有的鍵盤、滑鼠操作都是以訊息的形式傳送給各個應用程式的。GUI執行緒內部就有一個訊息佇列,GUI執行緒不斷的迴圈處理這些訊息,並根據訊息更新UI的呈現。如果這個時候,你讓GUI執行緒去處理一個耗時的操作(比如花10秒去下載一個檔案),那GUI執行緒就沒辦法處理訊息隊列了,UI介面就處於假死的狀態。

image

那我們該怎麼辦呢?不難想到使用執行緒,那線上程裡處理事件完成後,需要更新UI控制元件的狀態,又該怎麼辦呢?常用幾種方式:

① 使用GUI控制元件提供的方法,Winform是控制元件的Invoke方法,WPF中是控制元件的Dispatcher.Invoke方法

//1.Winform:Invoke方法和BeginInvoke
 this.label.Invoke(method, null); 

//2.WPF:Dispatcher.Invoke
 this.label.Dispatcher.Invoke(method, null);

② 使用.NET中提供的BackgroundWorker執行耗時計算操作,在其任務完成事件RunWorkerCompleted 中更新UI控制元件

using (BackgroundWorker bw = new BackgroundWorker())
{
    bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler((ojb,arg) =>
    {
        this.label.Text = "anidng";
    });
    bw.RunWorkerAsync();
}

③ 看上去很高大上的方法:使用GUI執行緒處理模型的同步上下文來送封UI控制元件修改操作,這樣可以不需要呼叫UI控制元件元素

.NET中提供一個用於同步上下文的類SynchronizationContext,利用它可以把應用程式模型連結到他的執行緒處理模型,其實它的本質還是呼叫的第一步中的方法。

實現程式碼分為三步,第一步定義一個靜態類,用於GUI執行緒的UI元素訪問封裝:

public static class GUIThreadHelper
{
    public static System.Threading.SynchronizationContext GUISyncContext
    {
        get { return _GUISyncContext; }
        set { _GUISyncContext = value; }
    }

    private static System.Threading.SynchronizationContext _GUISyncContext =
        System.Threading.SynchronizationContext.Current;

    /// <summary>
    /// 主要用於GUI執行緒的同步回撥
    /// </summary>
    /// <param name="callback"></param>
    public static void SyncContextCallback(Action callback)
    {
        if (callback == null) return;
        if (GUISyncContext == null)
        {
            callback();
            return;
        }
        GUISyncContext.Post(result => callback(), null);
    }

    /// <summary>
    /// 支援APM非同步程式設計模型的GUI執行緒的同步回撥
    /// </summary>
    public static AsyncCallback SyncContextCallback(AsyncCallback callback)
    {
        if (callback == null) return callback;
        if (GUISyncContext == null) return callback;
        return asynresult => GUISyncContext.Post(result => callback(result as IAsyncResult), asynresult);
    }
}

第二步,在主視窗註冊當前SynchronizationContext:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            CLRTest.ConsoleTest.GUIThreadHelper.GUISyncContext = System.Threading.SynchronizationContext.Current;
        }

第三步,就是使用了,可以在任何地方使用

GUIThreadHelper.SyncContextCallback(() =>
{
    this.txtMessage.Text = res.ToString();
    this.btnTest.Content = "DoTest";
    this.btnTest.IsEnabled = true;
});

 

  執行緒同步構造

多執行緒程式設計中很常用、也很重要的一點就是執行緒同步問題,掌握執行緒同步對臨界資源正確使用、執行緒效能有至關重要的作用!基本思路是很簡單的,就是加鎖嘛,在臨界資源的門口加一把鎖,來控制多個執行緒對臨界資源的訪問。但在實際開發中,根據資源型別不同、執行緒訪問方式的不同,有多種鎖的方式或控制機制(基元使用者模式構造和基元核心模式構造)。.NET提供了兩種執行緒同步的構造模式,需要理解其基本原理和使用方式。

基元執行緒同步構造分為:基元使用者模式構造和基元核心模式構造,兩種同步構造方式各有優缺點,而混合構造(如lock)就是綜合兩種構造模式的優點。

微笑 使用者模式構造

基元使用者模式比基元核心模式速度要快,她使用特殊的cpu指令來協調執行緒,在硬體中發生,速度很快。但也因此Windows作業系統永遠檢測不到一個執行緒在一個使用者模式構造上阻塞了。舉個例子來模擬一下使用者模式構造的同步方式:

  • 執行緒1請求了臨界資源,並在資源門口使用了使用者模式構造的鎖;
  • 執行緒2請求臨界資源時,發現有鎖,因此就在門口等待,並不停的去詢問資源是否可用;
  • 執行緒1如果使用資源時間較長,則執行緒2會一直執行,並且佔用CPU時間。佔用CPU幹什麼呢?她會不停的輪詢鎖的狀態,直到資源可用,這就是所謂的活鎖;

缺點有沒有發現?執行緒2會一直使用CPU時間(假如當前系統只有這兩個執行緒在執行),也就意味著不僅浪費了CPU時間,而且還會有頻繁的執行緒上下文切換,對效能影響是很嚴重的

當然她的優點是效率高,適合哪種對資源佔用時間很短的執行緒同步。.NET中為我們提供了兩種原子性操作,利用原子操作可以實現一些簡單的使用者模式鎖(如自旋鎖)。

System.Threading.Interlocked:易失構造,它在包含一個簡單資料型別的變數上執行原子性的讀寫操作。

Thread.VolatileRead 和 Thread.VolatileWrite:互鎖構造,它在包含一個簡單資料型別的變數上執行原子性的讀寫操作。

以上兩種原子性操作的具體內涵這裡就細說了(有興趣可以去研究文末給出的參考書籍或資料),針對題目11,來看一下題目程式碼:

int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =>
{
    a++; 
});
Console.Write(a);

上面程式碼是通過並行(多執行緒)來更新共享變數a的值,結果肯定是小於等於100000的,具體多少是不穩定的。解決方法,可以使用我們常用的Lock,還有更有效的就是使用System.Threading.Interlocked提供的原子性操作,保證對a的值操作每一次都是原子性的:

System.Threading.Interlocked.Add(ref a, 1);//正確

下面的圖是一個簡單的效能驗證測試,分別使用Interlocked、不用鎖、使用lock鎖三種方式來測試。不用鎖的結果是95,這答案肯定不是你想要的,另外兩種結果都是對的,效能差別卻很大。

image

為了模擬耗時操作,對程式碼稍作了修改,如下,所有的迴圈裡面加了程式碼Thread.Sleep(20);。如果沒有Thread.Sleep(20);他們的執行時間是差不多的。

System.Threading.Tasks.Parallel.For(0, 100, (i) =>
{
    lock (_obj)
    {
        a++; //不正確
        Thread.Sleep(20);
    }
});

吐舌笑臉 核心模式構造

這是針對使用者模式的一個補充,先模擬一個核心模式構造的同步流程來理解她的工作方式:

  • 執行緒1請求了臨界資源,並在資源門口使用了核心模式構造的鎖;
  • 執行緒2請求臨界資源時,發現有鎖,就會被系統要求睡眠(阻塞),執行緒2就不會被執行了,也就不會浪費CPU和執行緒上下文切換了;
  • 等待執行緒1使用完資源後,解鎖後會傳送一個通知,然後作業系統會把執行緒2喚醒。假如有多個執行緒在臨界資源門口等待,則會挑選一個喚醒;

看上去是不是非常棒!徹底解決了使用者模式構造的缺點,但核心模式也有缺點的:將執行緒從使用者模式切換到核心模式(或相反)導致巨大效能損失。呼叫執行緒將從託管程式碼轉換為核心程式碼,再轉回來,會浪費大量CPU時間,同時還伴隨著執行緒上下文切換,因此儘量不要讓執行緒從使用者模式轉到核心模式。

她的優點就是阻塞執行緒,不浪費CPU時間,適合那種需要長時間佔用資源的執行緒同步。

核心模式構造的主要有兩種方式,以及基於這兩種方式的常見的鎖:

  • 基於事件:如AutoResetEvent、ManualResetEvent
  • 基於訊號量:如Semaphore

吐舌笑臉 混合執行緒同步

既然核心模式和使用者模式都有優缺點,混合構造就是把兩者結合,充分利用兩者的優點,把效能損失降到最低。大概的思路很好理解,就是如果是在沒有資源競爭,或執行緒使用資源的時間很短,就是用使用者模式構造同步,否則就升級到核心模式構造同步,其中最典型的代表就是Lock了。

常用的混合鎖還不少呢!如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,這些鎖各有特點和鎖使用的場景。這裡主要就使用最多的lock來詳細瞭解下。

lock的本質就是使用的Monitor,lock只是一種簡化的語法形式,實質的語法形式如下:

bool lockTaken = false;
try
{
    Monitor.Enter(obj, ref lockTaken);
    //...
}
finally
{
    if (lockTaken) Monitor.Exit(obj);
}

那lock或Monitor需要鎖定的那個物件是什麼呢?注意這個物件才是鎖的關鍵,在此之前,需要先回顧一下引用物件的同步索引塊(AsynBlockIndex),這是前面文章中提到過的引用物件的標準配置之一(還有一個是型別物件指標TypeHandle),它的作用就在這裡了。

同步索引塊是.NET中解決物件同步問題的基本機制,該機制為每個堆內的物件(即引用型別物件例項)分配一個同步索引,她其實是一個地址指標,初始值為-1不指向任何地址。

  • 建立一個鎖物件Object obj,obj的同步索引塊(地址)為-1,不指向任何地址;
  • Monitor.Enter(obj),建立或使用一個空閒的同步索引塊(如下圖中的同步塊1),(圖片來源),這個才是真正的同步索引塊,其內部結構就是一個混合鎖的結構,包含執行緒ID、遞迴計數、等待執行緒統計、核心物件等,類似一個混合鎖AnotherHybridLock。obj物件(同步索引塊AsynBlockIndex)指向該同步塊1;
  • Exit時,重置為-1,那個同步索引塊1可以被重複利用;

381412-20150930224247574-1653709348

因此,鎖物件要求必須為一個引用物件(在堆上)。

吐舌笑臉 多執行緒使用及執行緒同步總結

首先還是儘量避免執行緒同步,不管使用什麼方式都有不小的效能損失。一般情況下,大多使用Lock,這個鎖是比較綜合的,適應大部分場景。在效能要求高的地方,或者根據不同的使用場景,可以選擇更符合要求的鎖。

在使用Lock時,關鍵點就是鎖物件了,需要注意以下幾個方面:

  • 這個物件肯定要是引用型別,值型別可不可呢?值型別可以裝箱啊!你覺得可不可以?但也不要用值型別,因為值型別多次裝箱後的物件是不同的,會導致無法鎖定;
  • 不要鎖定this,儘量使用一個沒有意義的Object物件來鎖;
  • 不要鎖定一個型別物件,因型別物件是全域性的;
  • 不要鎖定一個字串,因為字串可能被駐留,不同字元物件可能指向同一個字串;
  • 不要使用[System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.Synchronized)],這個可以使用在方法上面,保證方法同一時刻只能被一個執行緒呼叫。她實質上是使用lock的,如果是例項方法,會鎖定this,如果是靜態方法,則會鎖定型別物件;

 

  題目答案解析:

1. 描述執行緒與程序的區別?

  • 一個應用程式例項是一個程序,一個程序內包含一個或多個執行緒,執行緒是程序的一部分;
  • 程序之間是相互獨立的,他們有各自的私有記憶體空間和資源,程序內的執行緒可以共享其所屬程序的所有資源;

2. 為什麼GUI不支援跨執行緒訪問控制元件?一般如何解決這個問題?

因為GUI應用程式引入了一個特殊的執行緒處理模型,為了保證UI控制元件的執行緒安全,這個執行緒處理模型不允許其他子執行緒跨執行緒訪問UI元素。解決方法還是比較多的,如:

  • 利用UI控制元件提供的方法,Winform是控制元件的Invoke方法,WPF中是控制元件的Dispatcher.Invoke方法;
  • 使用BackgroundWorker;
  • 使用GUI執行緒處理模型的同步上下文SynchronizationContext來提交UI更新操作

上面幾個方式在文中已詳細給出。

3. 簡述後臺執行緒和前臺執行緒的區別?

應用程式必須執行完所有的前臺執行緒才可以退出,或者主動結束前臺執行緒,不管後臺執行緒是否還在執行,應用程式都會結束;而對於後臺執行緒,應用程式則可以不考慮其是否已經執行完畢而直接退出,所有的後臺執行緒在應用程式退出時都會自動結束。

通過將 Thread.IsBackground 設定為 true,就可以將執行緒指定為後臺執行緒,主執行緒就是一個前臺執行緒。

4. 說說常用的鎖,lock是一種什麼樣的鎖?

常用的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是一個混合鎖,其實質是Monitor['mɒnɪtə]。

5. lock為什麼要鎖定一個引數,可不可鎖定一個值型別?這個引數有什麼要求?

lock的鎖物件要求為一個引用型別。她可以鎖定值型別,但值型別會被裝箱,每次裝箱後的物件都不一樣,會導致鎖定無效。

對於lock鎖,鎖定的這個物件引數才是關鍵,這個引數的同步索引塊指標會指向一個真正的鎖(同步塊),這個鎖(同步塊)會被複用。

6. 多執行緒和非同步有什麼關係和區別?

多執行緒是實現非同步的主要方式之一,非同步並不等同於多執行緒。實現非同步的方式還有很多,比如利用硬體的特性、使用程序或纖程等。在.NET中就有很多的非同步程式設計支援,比如很多地方都有Begin***、End***的方法,就是一種非同步程式設計支援,她內部有些是利用多執行緒,有些是利用硬體的特性來實現的非同步程式設計。

7. 執行緒池的優點有哪些?又有哪些不足?

優點:減小執行緒建立和銷燬的開銷,可以複用執行緒;也從而減少了執行緒上下文切換的效能損失;在GC回收時,較少的執行緒更有利於GC的回收效率。

缺點:執行緒池無法對一個執行緒有更多的精確的控制,如瞭解其執行狀態等;不能設定執行緒的優先順序;加入到執行緒池的任務(方法)不能有返回值;對於需要長期執行的任務就不適合執行緒池。

8. Mutex和lock有何不同?一般用哪一個作為鎖使用更好?

Mutex是一個基於核心模式的互斥鎖,支援鎖的遞迴呼叫,而Lock是一個混合鎖,一般建議使用Lock更好,因為lock的效能更好。

9. 下面的程式碼,呼叫方法DeadLockTest(20),是否會引起死鎖?並說明理由。

public void DeadLockTest(int i)
{
    lock (this)   //或者lock一個靜態object變數
    {
        if (i > 10)
        {
            Console.WriteLine(i--);
            DeadLockTest(i);
        }
    }
}

不會的,因為lock是一個混合鎖,支援鎖的遞迴呼叫,如果你使用一個ManualResetEvent或AutoResetEvent可能就會發生死鎖。

10. 用雙檢鎖實現一個單例模式Singleton。

    public static class Singleton<T> where T : class,new()
    {
        private static T _Instance;
        private static object _lockObj = new object();

        /// <summary>
        /// 獲取單例物件的例項
        /// </summary>
        public static T GetInstance()
        {
            if (_Instance != null) return _Instance;
            lock (_lockObj)
            {
                if (_Instance == null)
                {
                    var temp = Activator.CreateInstance<T>();
                    System.Threading.Interlocked.Exchange(ref _Instance, temp);
                }
            }
            return _Instance;
        }
    }

11.下面程式碼輸出結果是什麼?為什麼?如何改進她?

int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =>
{
    a++; 
});
Console.Write(a);

輸出結果不穩定,小於等於100000。因為多執行緒訪問,沒有使用鎖機制,會導致有更新丟失。具體原因和改進在文中已經詳細的給出了。

 

版權所有,文章來源:http://www.cnblogs.com/anding 

自己總結:

程序(Process)是Windows系統中的一個基本概念,它包含著一個執行程式所需要的資源。 
一個正在執行的應用程式在作業系統中被視為一個程序,程序可以包括一個或多個執行緒。 
執行緒是作業系統分配處理器時間的基本單元,在程序中可以有多個執行緒同時執行程式碼。 
程序之間是相對獨立的,一個程序無法訪問另一個程序的資料(除非利用分散式計算方式), 
一個程序執行的失敗也不會影響其他程序的執行,Windows系統就是利用程序把工作劃分為多個獨立的區域的。 
程序可以理解為一個程式的基本邊界。是應用程式的一個執行例程,是應用程式的一次動態執行過程。

執行緒(Thread)是程序中的基本執行單元,是作業系統分配CPU時間的基本單位,一個程序可以包含若干個執行緒, 
在程序入口執行的第一個執行緒被視為這個程序的主執行緒。在.NET應用程式中,都是以Main()方法作為入口的, 
當呼叫此方法時系統就會自動建立一個主執行緒。 

應用程式域AppDomain)是一個程式執行的邏輯區域,它可以視為一個輕量級的程序,.NET的程式集正是在應用程式域中執行的一個程序可以包含有多個應用程式域,一個應用程式域也可以包含多個程式集。在一個應用程式域中包含了一個或多個上下文context,使用上下文 context CLR就能夠把某些特殊物件的狀態放置在不同容器當中。

上下文:

,在應用程式域當中,存在更細粒度的用於承載.NET物件的實體,那就.NET上下文Context。

 

同一個時刻,執行緒只會處於一個應用程式域內 

 

CLR的執行模型

 

公共語言執行時 CLR Common Language Runtime

是一個執行時環境,保證應用和底層作業系統之間必要的分離,是.NET Framework的主要執行引擎。是可由面向CLR的多種程式語言使用的執行時CLR的核心功能(記憶體管理、程式集載入、安全性、異常處理和執行緒同步等)由面向CLR的所有語言使用。

 

程式集是包含一個或多個型別定義檔案和資原始檔的集合.它允許我們分析可重用型別的邏輯表示和物理表示.

執行緒的開銷 

  • 執行緒核心物件(thread kernel object
    • 作業系統為建立的每個執行緒都會分配並初始化這種資料結構。資料結構包含一組對執行緒進行描述的屬性,還包含執行緒的上下文,包含模擬CPU暫存器的集合的記憶體塊。
  • 執行緒環境塊(thread environment blockTEB
    • TEB是在使用者模式下分配和初始化的記憶體塊。
    • TEB包含執行緒的異常處理鏈首。執行緒進入每個try塊都在鏈首插入一個節點,執行緒退出try塊從鏈中刪除該節點。
    • TEB還包含執行緒的執行緒本地儲存資料,以及由GDIOpenGL圖形使用的一些資料結構。
  • 使用者模式棧(user-mode stack
    • 使用者模式棧儲存傳給方法的區域性變數和實參。包含一個地址,指出當前方法返回,執行緒應該從什麼地方接著執行。
    • Windows預設為每個執行緒的使用者模式棧分配1MB的空間。 
  • 核心模式棧(kernel-mode stack
    • 應用程式程式碼向作業系統中的核心模式函式傳遞實參時,還會使用核心模式棧。針對傳給核心的任何實參,都會從使用者模式棧複製到核心模式棧。
  • DLL執行緒連線(attach)和執行緒分離(detach)通知
    • 任何時候在程序中建立執行緒,都會呼叫程序中載入的所有非託管DLLDLLMain方法,並向該方法傳遞DLL_THREAD_ATTACH標誌。
    • 任何時候在程序中執行緒終止,都會呼叫程序中載入的所有非託管DLLDLLMain方法,並向該方法傳遞DLL_THREAD_DETACH標誌
    •  

使用專用執行緒執行非同步的計算限制操作

以下介紹使用專用執行緒執行非同步的計算限制操作,但是建議避免使用此技術,而用執行緒池來執行非同步的計算限制操作。

如果執行的程式碼要求執行緒處於一種特定的狀態,而這種狀態對於執行緒池執行緒來說是非同尋常的,就可以考慮建立專用執行緒。

例如:

  • 執行緒需要以非普通執行緒優先順序執行。而所有執行緒池都以普通優先順序執行;雖然可以更改這一優先順序,但不建議這麼做。並且在不同的執行緒池操作之間,優先順序的更改是無法持續的
  • 需要執行緒表現為一個前臺執行緒,防止應用程式線上程任務結束前終止。執行緒池現場呢個始終為後臺執行緒。如果CLR想終止程序,那麼它們就完成不了任務。
  • 計算限制的任務需要長時間執行。執行緒池為了判斷是否需要建立一個額外的執行緒,所採用的邏輯是比較複雜的。直接為長時間執行的任務建立專用執行緒,就可以避免這一問題。
  • 要啟動執行緒,並可能呼叫ThreadAbort方法來提前終止它。

一個簡單的使用專用執行緒執行非同步操作的例子:

 

     static void Main(string[] args)

        {

            Thread 某執行緒 = new Thread(執行緒回撥函式);

            某執行緒.Start("hello");

            Console.WriteLine("某執行緒執行開始");

            某執行緒.Join();//join方法造成呼叫執行緒阻塞當前執行的任何程式碼,直到“某執行緒”銷燬或者終止

            Console.WriteLine("繼續執行");

            Console.Read();

        }

        private static void 執行緒回撥函式(Object 狀態引數) {

            Thread.Sleep(10000);

            if (狀態引數.GetType() == typeof(string))

            {

                Console.WriteLine("這是一個字串");

            }

            else {

                Console.WriteLine("未識別");

            }

        }

 

使用執行緒的理由

  • 可響應性
    • 在客戶端GUI應用程式中,可以將一些工作交給執行緒進行,使GUI執行緒能靈敏響應使用者輸入。
  • 效能
    • 在多個CPU或多核CPU上使用多執行緒會提升效能,因為它可以真正意義上同時執行多件事情

 

前面的這一段建立一個返回值為boolTask物件

Task<bool> t = new Task<bool> (...);

括號中的語句建立一個lambda表示式,可以理解為一個匿名方法。

() => { ...; return true; }

 小括號代表這個匿名方法沒有引數,return true表示返回一個bool。在這裡,這個匿名函式作為引數傳入Task的建構函式,就得到了:

Task<bool> t = new Task<bool> ( () => { ...; return true; } );
 
Task<int> t1 = new Task<int>(n =>
{
    System.Threading.Thread.Sleep(1000);
    return (int)n;
}, 1000);
//定製一個延續任務計劃
t1.ContinueWith(task =>
{
    Console.WriteLine("end" + t1.Result);
}, TaskContinuationOptions.AttachedToParent);
t1.Start();
 
兩個引數的lamber表示式第一個引數需要加{}
 
 var t1 = new Task(() => TaskMethod("Task 1"));
  t1.Start();
  Task.WaitAll(t1);//等待所有任務結束  
  Task.Run(() => TaskMethod("Task 2")); 
  Task.Factory.StartNew(() => TaskMethod("Task 3")); 直接非同步的方法  
  var t3=Task.Factory.StartNew(() => TaskMethod("Task 3"));
  Task.WaitAll(t3);//等待所有任務結束
 

 

TPL(Task Parallel Library)  並行程式設計

 

並行的特點Task Parallel Library 內部有  Task例項, 所有任務完成返回,少量但時間任務不要使用並行,因為裡面也有效能開銷,任務的排程,建立方法的委託

 

Parallel.For()Parallel.ForEach()方法在每次迭代中呼叫相同的程式碼,而Parallel.Invoke()方法允許同時呼叫不同的方法。Parallel.Invoke用於任務並行性,而Parallel.ForEach用於資料並行性。

‘’‘’

     

 

  ParallelLoopResult result = Parallel.For(0, 10, i =>
            {
                Console.WriteLine(i)
                Thread.Sleep(10);
            });

 

 

 

string[] data = { "str1", "str2", "str3" };
            ParallelLoopResult result = Parallel.ForEach<string>(data, str =>
              {
                  Console.WriteLine(str);
              });

 

0 10代表開始 結束下標

I代表下標

Data代表List資料  Str 代表其中一個數據

I  str 引數

 

 

 

Task..WaitAll 是阻塞當前執行緒等待其它任務完畢的意思;Task .WhenAll 是建立一個非同步(不阻塞當前執行緒)的Task

 

傳遞引數 比如

 //等待兩個任務執行完成(同時執行),再執行主執行緒
Task.WaitAll(task1, task2);

 

System.ICloneable介面支援克隆,即用與現有例項相同的值建立類的新例項

 

 

volatile的作用是: 作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值.