.NET面試題解析(07)-多線程編程與線程同步

關於線程的知識點實際上是不少的,好比多線程編程、線程上下文、異步編程、線程同步構造、GUI的跨線程訪問等等,本文只是從常見面試題的角度(也是開發過程當中經常使用)去深刻淺出線程相關的知識。若是想要系統的學習多線程,沒有捷徑的,也不要偷懶,仍是去看專業書籍的比較好。html

  常見面試題目:

1. 描述線程與進程的區別?面試

2. 爲何GUI不支持跨線程訪問控件?通常如何解決這個問題?編程

3. 簡述後臺線程和前臺線程的區別?緩存

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

5. lock爲何要鎖定一個參數,可不可鎖定一個值類型?這個參數有什麼要求?網絡

6. 多線程和異步有什麼關係和區別?多線程

7. 線程池的優勢有哪些?又有哪些不足?異步

8. Mutex和lock有何不一樣?通常用哪個做爲鎖使用更好?異步編程

9. 下面的代碼,調用方法DeadLockTest(20),是否會引發死鎖?並說明理由。post

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

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

11.下面代碼輸出結果是什麼?爲何?如何改進她?

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

  線程基礎

微笑 進程與線程

咱們運行一個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

我的能力有限,本文內容僅供學習、探討,歡迎指正、交流。

.NET面試題解析(00)-開篇來談談面試 & 系列文章索引

  參考資料:

書籍:CLR via C#

書籍:你必須知道的.NET

.NET基礎拾遺(5)多線程開發基礎

概括一下:C#線程同步的幾種方法

C#並行編程-相關概念

多線程之旅七——GUI線程模型,消息的投遞(post)與處理(IOS開發前傳)

C# 溫故而知新: 線程篇(一)

相關文章
相關標籤/搜索