【C#多線程】1.Thread類的使用及注意要點


Thread隨便講講

  因為在C#中,Thread類在我們的新業務上並不常用了(因為創建一個新線程要比直接從線程池拿線程更加耗費資源),並且在.NET4.0后新增了Task類即Async與await關鍵字,使得我們基本不再用Thread了,不過在學習多線程前,有必要先了解下Thread類,這里就先隨便講講Thread。

1.使用多線程的幾種方式

  多線程Thread類只支持運行兩種方法,一種是無參數並且無返回值的方法,第二種是有一個Object類型參數(有且只能有一個參數,並且必須是Object類型)且無返回值的方法。如果想讓多線程方法攜帶多個參數,可以將多個參數放入一個集合或數組中傳入方法。

  下面例子使用了控制台來演示多線程的簡單使用:

using System; using System.Threading; namespace ConsoleApplication1 { class Program { //無參數無返回值方法
        public static void DoSomething() { for (int i = 0; i < 100; i++) { Thread.Sleep(500); } } //有參數無返回值方法
        public static void DoSomethingWithParameter(object obj) { for (int i = 0; i < (int)obj; i++) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Thread.Sleep(500); } } static void Main(string[] args) { //獲取主線程ID
            int currentThreadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"---------主線程<{currentThreadId}>開始運行---------"); //多線程運行無參數方法方式1
            ThreadStart ts = DoSomething;//ThreadStart是一個無參數,無返回值的委托
            Thread thread1 = new Thread(ts); thread1.Start(); //多線程運行無參數方法方式2
            Thread thread2 = new Thread(DoSomething);//可省略ThreadStart
 thread2.Start(); //多線程運行有參數方法方式1 //ParameterizedThreadStart是一個有一個Object類型參數,但是無返回值的委托。
            ParameterizedThreadStart pts = DoSomethingWithParameter; Thread thread3 = new Thread(pts); thread3.Start(100); //多線程運行有參數方法方式2 //可以省略ParameterizedThreadStart
            Thread thread4 = new Thread(DoSomethingWithParameter); thread4.Start(100); //還可以使用lamda表達式簡化多線程寫法
            new Thread(() => { for (int i = 0; i < 100; i++) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Thread.Sleep(500); } }).Start(); Console.WriteLine($"---------主線程<{currentThreadId}>運行結束---------"); } } }

  運行結果如下:

  

 

 2.前台線程與后台線程

  • 前台線程

  如主線程(或稱為UI線程)就是前台線程,默認Thread的實例均為前台線程,前台線程的特點是,如果當前應用的前台線程沒有全部運行完畢,那么當前應用就無法退出。舉個例子,我們知道正常情況下,控制台應用在Main方法結束后會自動結束當前進程,如果我們在Main方法中創建了一個新Thread線程,並使其保持運行,那么即使Main方法執行完畢,控制台進程也無法自動關閉(除非手動右上角點×)。就如下圖情況,畫紅圈的地方表示Main方法執行完畢,可是程序依舊在運行,所以我們一般在用Thread的時候會將Thread設置為后台線程。

 

  • 后台線程

  后台線程與前台線程的唯一區別是,它不會去影響程序的生老病死,當程序的前台線程全部關閉(即程序退出),那么即使程序的后台線程依舊在執行任務,那么也會強制關閉。

  設置Thread為后台線程的方式:

        Thread tt = new Thread(DoSomething); tt.IsBackground = true;//設置tt為后台線程
        tt.Start();

  前台線程與后台線程對程序的影響效果看似好像不算大,但是如果我們在做Winform或者WPF項目時,若在某窗體內執行一個新線程任務(這個新線程是前台線程),如果在任務執行期間關閉程序,此時會發現,雖然界面都被關閉,但是計算機任務管理器中此程序依舊還在運行(並且如果在新線程中執行的任務異常導致線程無法關閉,那么這個程序就會一直在后台跑下去),再次開啟程序可能會導致打不開等后果,這種行為是非常不好的。所以我們一般使用多線程Thread類時,最好順手將它設置為后台線程。我們可以舉個例子。

        static void Main(string[] args) { //獲取主線程ID
            int currentThreadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"---------主線程<{currentThreadId}>開始運行---------"); //執行一個大概可以運行50秒的新線程
            Thread t = new Thread(() => { for (int i = 0; i < 100; i++) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Thread.Sleep(500); } }); t.IsBackground = true;//設置t為后台線程
 t.Start(); Console.WriteLine($"---------主線程<{currentThreadId}>運行結束---------"); }

  這個例子的運行結果就不截圖了,因為控制台會一閃而過(立即執行完Main方法便關閉),即使后台線程t還在執行任務,但是也會強制關閉。

 

3.讓主線程等待新線程執行完成后再繼續執行(使用Thread的Join方法)

  直接上代碼:

        static void Main(string[] args) { //獲取主線程ID
            int currentThreadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"---------主線程<{currentThreadId}>開始運行---------"); //執行一個大概可以運行50秒的新線程
            Thread t = new Thread(() => { for (int i = 0; i < 20; i++) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Thread.Sleep(500); } }); t.IsBackground = true;//設置t為后台線程
 t.Start(); t.Join();//在t線程執行期間,如果主線程調用t線程的Join方法,主線程會卡在這個地方直到t線程執行完畢
 Console.WriteLine($"---------主線程<{currentThreadId}>運行結束---------"); }

 

4.Thread實例的其他常用方法

  直接看代碼注釋吧:

        static void Main(string[] args) { //執行一個大概可以運行50秒的新線程
            Thread t = new Thread(DoSth); t.IsBackground = true;//設置t為后台線程
 t.Start(); t.Join();//在t線程執行期間,如果主線程調用t線程的Join方法,主線程會卡在這個地方知道t線程執行完畢
            t.Priority = ThreadPriority.Normal;//設置線程調度的優先級
            ThreadState rhreadState = t.ThreadState;//獲取線程運行狀態。
            bool b = t.IsAlive;//獲取線程當前是否存活
            t.Interrupt();//中斷當前線程
            t.Abort();//終止線程 
        }

 

5.Thread類的常用方法

  直接看代碼注釋吧:

        static void Main(string[] args) { //使得當前線程暫停1秒再繼續執行,此處會暫停主線程1秒鍾 //如果寫在其他線程執行的方法中,會讓執行那個方法的線程暫停1秒再繼續執行)
            Thread.Sleep(1000); //獲取當前執行線程的線程實例
            Thread t = Thread.CurrentThread; }

  

6.使用多線程需要注意的要點

   (1)子線程不可以直接調用UI線程(即主線程)的UI對象,但是可以調用在主線程自定義的對象

  我們在做Winform或WPF開發時,例如在前端有一個TextBox文本框,其Name屬性為textBox,那么如果我們在此窗體內開啟了個子線程,並在子線程內對textBox.Text賦值,是會報錯的,因為子線程無法訪問主線程的UI元素(實質是UI元素必須由創建它的線程去操作)。

  如下代碼,子線程操作主線程創建的對象時不會報錯,但是子線程操作主線程創建的UI對象時會報錯:

        private void button1_Click(object sender, EventArgs e) { Student stu = new Student();//主線程創建的Student類實例
            new Thread(() => { stu.Name = "ccc";//子線程操作主線程創建的對象並不會報錯。
                textBox1.Text = "abc";//子線程直接調用UI線程textBox1會報錯
 }).Start(); }

 

   解決思路:在子線程想操作UI線程的UI元素時,呼叫主線程去操作即可,代碼如下:

        delegate void DoSth(string str);//創建一個委托
        public void SetTextBox(string str)//創建一個委托方法用於改變主線程textBox的值
 { textBox1.Text = str; }
     //按鈕點擊事件
private void button1_Click(object sender, EventArgs e) {
       //在子線程內執行....
new Thread(() => { //----------------------詳細寫法------------------------ DoSth delegateMethod = new DoSth(SetTextBox);//創建方法的委托 //this指當前Window //this.Invoke指讓創建窗體的線程執行某委托方法 //第二個參數是傳入委托方法即SetTextBox的參數 this.Invoke(delegateMethod, "abc"); //----------------------簡寫方式---------------------- this.Invoke(new Action(() => { textBox1.Text = "abc";//子線程直接調用UI線程textBox1會報錯 })); }).Start();

  補充:上面代碼是Winform跨線程操作UI元素的常用方式,那么WPF怎么跨線程操作UI呢?直接看下面代碼吧

    //方式1(常用):獲取當前應用的UI線程,執行某方法
    App.Current.Dispatcher.Invoke(() => { textBox1.Text="abc" }); //方式2(只能在this是當前Window或可以獲取到窗體實例的情況下使用):
    this.Dispatcher.Invoke(new Action(()=> { textBox1.Text="abc" })); 

  (2)多線程同時訪問一個資源時,要注意同步問題。

  比如兩個及以上的線程同時訪問一個資源(可以是文件,可以是對象),如果沒有注意同步問題,會導致以下問題。直接看代碼

        private void button1_Click(object sender, EventArgs e) { int num = 0; //創建兩個線程對num進行累加,各加100000,理論上線程執行完畢后最后的值應該是200000
            Thread t1 = new Thread(() => { for (int i = 0; i < 100000; i++) { num++; } }); Thread t2 = new Thread(() => { for (int i = 0; i < 100000; i++) { num++; } }); //兩個子線程同時執行
 t1.Start(); t2.Start(); //等待t1與t2線程執行完畢再繼續執行
 t1.Join(); t2.Join(); Console.WriteLine(num.ToString());//輸出num的值 }

  結果:

  結果並不是200000,因為t1與t2線程同時對num進行自增操作時候,經常會出現t1讀取到了num為99,自增1,結果100賦值給num,但是在t1剛讀取到num值並且還沒進行自增操作時,t2也讀取到了num為99,自增下也是100賦值給num。也就是說t1與t2進行了相同的操作。

  

 

   如何避免當前這個問題呢?就需要在多線程訪問一個資源時,進行資源同步處理。那什么是同步呢?同步是指我用完了你才能用,你我不能同時使用一個資源。這個問題的詳細解決方法會在該系列以后的博客中寫。

 

  

  下節我們會簡單講講線程池+以前的兩種異步處理機制。


免責聲明!

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



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