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進行了相同的操作。
如何避免當前這個問題呢?就需要在多線程訪問一個資源時,進行資源同步處理。那什么是同步呢?同步是指我用完了你才能用,你我不能同時使用一個資源。這個問題的詳細解決方法會在該系列以后的博客中寫。
下節我們會簡單講講線程池+以前的兩種異步處理機制。