由於在C#中,Thread類在咱們的新業務上並不經常使用了(由於建立一個新線程要比直接從線程池拿線程更加耗費資源),而且在.NET4.0後新增了Task類即Async與await關鍵字,使得咱們基本再也不用Thread了,不過在學習多線程前,有必要先了解下Thread類,這裏就先隨便講講Thread。前端
多線程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}>運行結束---------"); } } }
運行結果以下:異步
如主線程(或稱爲UI線程)就是前臺線程,默認Thread的實例均爲前臺線程,前臺線程的特色是,若是當前應用的前臺線程沒有所有運行完畢,那麼當前應用就沒法退出。舉個例子,咱們知道正常狀況下,控制檯應用在Main方法結束後會自動結束當前進程,若是咱們在Main方法中建立了一個新Thread線程,並使其保持運行,那麼即便Main方法執行完畢,控制檯進程也沒法自動關閉(除非手動右上角點×)。就以下圖狀況,畫紅圈的地方表示Main方法執行完畢,但是程序依舊在運行,因此咱們通常在用Thread的時候會將Thread設置爲後臺線程。學習
後臺線程與前臺線程的惟一區別是,它不會去影響程序的生老病死,當程序的前臺線程所有關閉(即程序退出),那麼即便程序的後臺線程依舊在執行任務,那麼也會強制關閉。this
設置Thread爲後臺線程的方式:spa
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還在執行任務,可是也會強制關閉。code
直接上代碼:orm
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}>運行結束---------"); }
直接看代碼註釋吧:
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();//終止線程
}
直接看代碼註釋吧:
static void Main(string[] args) { //使得當前線程暫停1秒再繼續執行,此處會暫停主線程1秒鐘 //若是寫在其餘線程執行的方法中,會讓執行那個方法的線程暫停1秒再繼續執行)
Thread.Sleep(1000); //獲取當前執行線程的線程實例
Thread t = Thread.CurrentThread; }
(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進行了相同的操做。
如何避免當前這個問題呢?就須要在多線程訪問一個資源時,進行資源同步處理。那什麼是同步呢?同步是指我用完了你才能用,你我不能同時使用一個資源。這個問題的詳細解決方法會在該系列之後的博客中寫。
下節咱們會簡單講講線程池+之前的兩種異步處理機制。