C# 多線程 詳解


【基礎篇】

  • 怎樣創建一個線程
  • 受托管的線程與Windows線程
  • 前台線程與后台線程
  • 名為BeginXXX和EndXXX的方法是做什么用的
  • 異步和多線程有什么關聯

【WinForm多線程編程篇】

  • 多線程WinForm程序總是拋出InvalidOperationException,怎么解決
  • Invoke和BeginInvoke干什么用的,內部是怎么實現的
  • 每個線程都有消息隊列嗎
  • 為什么WinForm不允許跨線程修改UI線程控件的值
  • 有沒有什么辦法可以簡化WinForm多線程的開發

【線程池】

  • 線程池的作用是什么
  • 所有進程使用一個共享的線程池,還是每個進程使用獨立的線程池
  • 線程池中線程的分類
  • .NET線程池有什么不足

【同步】

  • CLR怎樣實現lock(obj)鎖定
  • 互斥對象(Mutex)、事件(Event)對象與lock語句的比較

基礎篇

怎樣創建一個線程

方法一:使用Thread類

   public static void Main(string[] args)
        {
            //方法一:使用Thread類
            ThreadStart threadStart = new ThreadStart(Calculate);//通過ThreadStart委托告訴子線程執行什么方法                        Thread thread = new Thread(threadStart);
            thread.Start();//啟動新線程
        }

        public static void Calculate()
        {
            Console.Write("執行成功");
            Console.ReadKey();
        }

方法二:使用Delegate.BeginInvoke

   delegate double CalculateMethod(double r);//聲明一個委托,表明需要在子線程上執行的方法的函數簽名
        static CalculateMethod calcMethod = new CalculateMethod(Calculate);

   static void Main(string[] args)
        {
            //方法二:使用Delegate.BeginInvoke
            //此處開始異步執行,並且可以給出一個回調函數(如果不需要執行什么后續操作也可以不使用回調)
            calcMethod.BeginInvoke(5, new AsyncCallback(TaskFinished), null);
            Console.ReadLine();
        }

   public static double Calculate(double r)
        {
            return 2 * r * Math.PI;
        }
        //線程完成之后回調的函數
        public static void TaskFinished(IAsyncResult result)
        {
            double re = 0;
            re = calcMethod.EndInvoke(result);
            Console.WriteLine(re);
        }

方法三:使用ThreadPool.QueueworkItem

受托管的線程與Windows線程

  .NET應用的線程實際上仍然是Windows線程。但是,當某個線程被CLR所知時,我們將它稱為受托管的線程。具體來說,由受托管的代碼創建出來的線程就是受托管的線程。不過,一旦該線程執行了受托管的代碼它就變成了受托管的線程。

  一個受托管的線程和非受托管的線程的區別在於,CLR將創建一個System.Threading.Thread類的實例來代表並操作前者。在內部實現中,CLR將一個包含了所有受托管線程的列表保存在一個叫做ThreadStore地方。

  CLR確保每一個受托管的線程在任意時刻都在一個AppDomain中執行,但是這並不代表一個線程將永遠處在一個AppDomain中,它可以隨着時間的推移轉到其他的AppDomain中。

前台線程與后台線程

  啟動了多個線程的程序在關閉的時候卻出現了問題,如果程序退出的時候不關閉線程,那么線程就會一直的存在,但是大多啟動的線程都是局部變量,不能一一的關閉,如果調用Thread.CurrentThread.Abort()方法關閉主線程的話,就會出現ThreadAbortException異常。可以通過這個方法:Thread.IsBackground設置線程為后台線程。

  msdn對前台線程和后台線程的解釋:托管線程或者是后台線程,或者是前台線程。后台線程不會是托管執行環境處於活動狀態,除此之外,后台線程與前台線程是一樣的。一旦所有前台進程在托管進程(其中.exe文件時托管程序集)中被停止,系統將停止所有后台線程並關閉。通過設置Thread.IsBackground屬性,可以將一個線程指定為后台線程或者前台線程。從非托管代碼進入托管執行環境的所有線程都被標記為后台線程。通過創建並啟動新的Thread對象而生成的所有線程都是前台線程。

名為BeginXXX和EndXXX的方法是做什么用的

  這是.net的一個異步方法名稱規范。

  .net在設計的時候為異步編程設計了一個異步編程模型(APM),比如所有的Stream就是BeginRead,EndRead,Socket,WebRequet,SqlCommand都運用到了這個模式,一般來講,調用BeginXXX的時候,一般會啟動一個異步過程去執行一個操作,EndInvoke可以接受這個異步操作的返回,當然如果異步操作在EndIncoke調用的時候還沒有執行完成,EndInvoke會一直等待異步操作完成或者超時。

  .NET的異步編程模型(APM)一般包含BeginXXX,EndXXX,IAsyncResult這三個元素,BeginXXX方法都有返回一個IAsyncResult,而EndXXX都需要接受一個IAsyncResult作為參數。

異步和多線程

  異步有許多種方法,我們可以用進程來做異步,或者使用線程,或者硬件的一些特性,比如在實現異步IO的時候,可以以下兩種方案:

  方案一:可以通過初始化一個子線程,然后在子線程里進行IO,而讓主線程順利往下執行,當子線程執行完畢就回調

  方案二:使用硬件的支持(現在許多硬件都有自己的處理器),來實現完全的異步,這時我們只需將IO請求告知硬件驅動程序,然后迅速返回,然后等着硬件IO就緒通知我們就可以了

WinForm多線程編程篇 

多線程WinForm程序總是拋出InvalidOperationException,怎么解決

  在WinForm中使用線程時,常常遇到一個問題,當在子線程(非UI線程)中修改一個空間的值:比如修改進度條進度,時會拋出異常。

  解決方法就是利用控件提供的Invoke和BeginInvoke把調用封送回UI線程,也就是讓控件屬性修改在UI線程上執行。

  例如:

   delegate void changeText(double result);
        public Form1()
        {
            InitializeComponent();
            ThreadStart threadStart = new ThreadStart(Calculate);
            Thread thread = new Thread(threadStart);
            thread.Start();
        }

        public void Calculate()
        {
            double r = 2;
            double result = 2 * Math.PI * r;
            CalcFinished(result);
        }
        public void CalcFinished(double result)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new changeText(CalcFinished), result);
            }
            else
            {
                this.textBox1.Text = result.ToString();
            }
        }

  這里用到了Control的一個屬性InvokeRequired(這個屬性石可以在其它線程里訪問),這個屬性表明調用是否來自非UI線程,如果是,使用BeginInvoke來調用這個函數,否則就直接調用,省去線程封送的過程。

Invoke和BeginInvoke干什么用的,內部是怎么實現的

  這兩個方法主要是讓給出的方法在控件創建的線程上執行。

  Invoke使用了Win32API的SendMessage     BeginInvoke使用了Win32API的PostMessage

  這兩個方法想UI線程的消息隊列中放入一個消息,當UI線程處理這個消息時,就會在自己的上下文中執行傳入的方法,換句話說,凡是使用BeginInvoke和Invoke調用的線程都是在UI主線程中執行,所以如果這些方法里涉及一些靜態變量,不用考慮加鎖的問題。

每個線程都有消息隊列嗎?

  不是,知識創建了窗體對象的線程才會有消息隊列(下面是《Windows核心編程》關於這一段的描述)

  當一個線程第一希被建立時,系統假定線程不會被用於任何與用戶相關的任務。這樣可以減少線程對系統資源的要求。但是,一旦這個線程調用一個與圖形用戶界面有關的函數(例如檢查它的消息隊列或建立一個窗口),系統就會為該線程分配一些另外的資源,以便它能夠執行與用戶界面有關的任務。特別是,系統分配一個THREADINFO結構,並將這個數據結構與線程聯系起來。

  這個THREADINFO結構包含一組成員變量,利用這組成員,線程可以認為它是在自己獨占的環境中運行。THREADINFO是一個內部的、未公開的數據結構,用來指定線程的登記消息隊列(posted-message queue)、發送消息隊列(send-message queue)、應答消息隊列(reply-message queue)、虛擬輸入隊列(virtualized-input queue)、喚醒標志(wake flag)以及用來描述線程局部輸入狀態的若干變量。

為什么WinForm不允許跨線程修改UI線程控件的值

  vs2005及以上版本,當在Visual Studio調試器中運行代碼時,如果您從一個線程訪問某個UI元素,而該線程不是創建該UI元素時所在的線程,則會引發InvalidOperationException調試器引發該異常以警告您存在危險的編程操作。UI元素不是線程安全的,所以只應在創建它們的線程上進行訪問。

有沒有什么辦法可以簡化WinForm多線程的開發

  使用backgroundworker,使用這個組件可以避免回調時的Invoke和BeginInvoke,並且提供了許多豐富的方法和事件

線程池

線程池的作用是什么

  減小線程創建和銷毀的開銷

   創建線程涉及到用戶模式和內核模式的切換,內存分配,dll通知等一系列過程,線程銷毀的步驟也是開銷很大的,所以如果應用程序使用完一個線程,我們能把線程暫時存放起來,以備下次使用,就可以減小這些開銷。

 所有進程使用一個共享的線程池,還是每個進程使用獨立的線程池

  每個進程都有一個線程池,一個進程中只能有一個實例,它在各個應用程序域(AppDomain)是共享的,線程池僅僅保留相當少的線程,保留的線程可以用SetMinThread這個方法設置,當程序需要一個線程時,線程池中沒有空閑的線程時,線程池就會負責創建這個線程,調用完后,不會立即銷毀,而是把它放在池子里,以備下次使用,但是,如果超出一定時間沒使用,線程池就會回收線程,所以線程池里存在的線程數實際是個動態的過程。

線程池中線程的分類

  線程池里的線程按照公用被分成了兩大類:工作線程和IO線程(IO完成線程),前者用於執行普通操作,后者專用於異步IO。它們分別在什么情況下被使用,二者工作原理有什么不同?通過下面這個例子,我們用一個流讀出一個很大的文件(文件大,操作時間長,便於觀察),然后用另一個輸出流把所讀出的文件的一部分寫到磁盤上。

  用兩種方法創建輸出流,分別是:

  創建一個異步的流(注意構造函數最后那個true)

  創建一個同步流

   string readPath = "d:\\工作常用軟件\\VS2012Documentation.iso";
        string writePath = "d:\\vs2012.ios";
        byte[] buffer = new byte[90000000];
        //創建一個異步流
        FileStream outputfs = new FileStream(writePath, FileMode.Create, FileAccess.Write, FileShare.None, 256, true);
        Console.WriteLine("異步流");
        //創建一個同步流
        //FileStream outputfs = File.OpenWrite(writePath);
        //Console.WriteLine("同步流");
        //然后在寫文件期間查看線程池的狀況
        ShowThreadDetail("初始狀態");
        FileStream fs = File.OpenRead(readPath);
        fs.BeginRead(buffer, 0, 90000000, delegate(IAsyncResult o)
        {
            outputfs.BeginWrite(buffer, 0, buffer.Length, delegate(IAsyncResult o1)
            {
                Thread.Sleep(1000);
                ShowThreadDetail("BeginWrite的回調線程");
            }, null);
            Thread.Sleep(500);
        },
        null);
        Console.ReadLine();

   public static void ShowThreadDetail(string caller)
        {
            int IO;
            int Worker;
            ThreadPool.GetAvailableThreads(out Worker, out IO);
            Console.WriteLine("Worker:{0};IO:{1}", Worker, IO);
        }

  輸出結果

  異步流

  Worker:1023;    IO:1000

  Worker:1023;    IO:999

  同步流

  Worker:1023;    IO:1000

  Worker:1022;    IO:1000

  這兩個構造函數創建的流都可以使用BeginWrite來異步寫數據,但二者行為不同,當使用同步的流進行異步寫時,通過回調的輸出我們可以看到,它使用的是工作線程,而非IO線程,而異步流使用IO線程。

.NET線程池有什么不足

   沒有提供方法控制加入線程池的線程:一旦加入線程池,我們沒辦法掛起,終止這些線程,唯一可以做的就是等他自己執行

  • 不能為線程設置優先級
  • 所支持的Callback不能有返回值。WaitCallback只能帶一個object類型的參數
  • 不適合用於長期執行某任務的場合

 

同步

CLR怎樣實現lock(obj)鎖定

   從原理上講,lock和Syncronized Attribute都是用Moniter.Enter實現的,例如:

  object obj = new object();

  lock(obj){

  //do things...

  }

  在編譯時,會被編譯為類似

  try{

    Moniter.Enter(obj){

    //do things...

    }

  }

  catch{...}

  finally{

    Moniter.Exit(obj);

  }

  每個對象實例頭部都有一個指針,這個指針指向的結構包含了對象的鎖定信息,當第一次使用Moniter.Enter(obj)是,這個obj對象的鎖定結構就會被初始化,第二次調用時,會檢驗這個object的鎖定結構,如果鎖沒有被釋放,則調用會阻塞。

互斥對象(Mutex)、事件(Event)對象與lock語句的比較

  這里所謂的事件是一種用於同步的內核機制,互斥對象和事件對象屬於內核對象,利用內核對象進行線程同步,線程必須要在用戶模式和內核模式間切換,所有一般效率很低,但利用互斥對象和事件對象這樣的內核對象,可以在多個線程中的各個線程間進行同步。

  lock或者Moniter是.net用於一種特殊結構實現的,不涉及模式切換,就是工作在用戶方式下,同步速度較快,但是不能跨進程同步。

 

 

 

 

 


免責聲明!

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



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